@starim-io/bot-sdk 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1188 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs');
4
+ var path = require('path');
5
+ var http = require('http');
6
+ var crypto = require('crypto');
7
+
8
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
+
10
+ var path__default = /*#__PURE__*/_interopDefault(path);
11
+
12
+ // src/errors.ts
13
+ var StarIMApiError = class extends Error {
14
+ code;
15
+ statusCode;
16
+ responseBody;
17
+ constructor(opts) {
18
+ super(opts.message);
19
+ this.name = "StarIMApiError";
20
+ this.statusCode = opts.statusCode;
21
+ this.code = opts.code;
22
+ this.responseBody = opts.responseBody;
23
+ }
24
+ };
25
+ var StarIMSignatureError = class extends Error {
26
+ constructor(message = "Invalid X-StarIM-Signature") {
27
+ super(message);
28
+ this.name = "StarIMSignatureError";
29
+ }
30
+ };
31
+ async function putToS3(creds, body) {
32
+ const headers = {
33
+ "Content-Type": creds.contentType || "application/octet-stream"
34
+ };
35
+ const ac = new AbortController();
36
+ const to = setTimeout(() => ac.abort(), 12e4);
37
+ try {
38
+ const res = await fetch(creds.uploadUrl, {
39
+ method: creds.method || "PUT",
40
+ headers,
41
+ body,
42
+ signal: ac.signal
43
+ });
44
+ if (!res.ok) {
45
+ const t = await res.text().catch(() => "");
46
+ throw new Error(`S3 upload failed: ${res.status} ${t.slice(0, 200)}`);
47
+ }
48
+ } finally {
49
+ clearTimeout(to);
50
+ }
51
+ }
52
+ async function uploadBuffer(client, buffer, fileName, fileType) {
53
+ const creds = await client.issueUploadCredentials({
54
+ fileName,
55
+ fileSize: buffer.length,
56
+ fileType: fileType || "",
57
+ metadata: {}
58
+ });
59
+ await putToS3(creds, buffer);
60
+ return client.completeUpload({
61
+ key: creds.key,
62
+ fileName,
63
+ fileSize: buffer.length,
64
+ fileType: fileType || creds.contentType,
65
+ category: "bot",
66
+ isPublic: false
67
+ });
68
+ }
69
+ async function uploadFileFromPath(client, filePath, options) {
70
+ const buffer = await fs.promises.readFile(filePath);
71
+ const fileName = options?.fileName || path__default.default.basename(filePath);
72
+ return uploadBuffer(client, buffer, fileName, options?.fileType);
73
+ }
74
+ async function uploadFromUrl(client, sourceUrl, options) {
75
+ const res = await fetch(sourceUrl);
76
+ if (!res.ok) {
77
+ throw new Error(`Failed to fetch ${sourceUrl}: ${res.status}`);
78
+ }
79
+ const buf = Buffer.from(await res.arrayBuffer());
80
+ const fileName = options?.fileName || (() => {
81
+ try {
82
+ const u = new URL(sourceUrl);
83
+ return path__default.default.basename(u.pathname) || "download.bin";
84
+ } catch {
85
+ return "download.bin";
86
+ }
87
+ })();
88
+ return uploadBuffer(client, buf, fileName, options?.fileType || res.headers.get("content-type") || "");
89
+ }
90
+ async function sendPhotoFromPath(client, chatId, filePath, options) {
91
+ const done = await uploadFileFromPath(client, filePath, {
92
+ fileName: options?.fileName,
93
+ fileType: options?.fileType
94
+ });
95
+ return client.sendPhoto({
96
+ chat_id: chatId,
97
+ file_id: done.id,
98
+ caption: options?.caption
99
+ });
100
+ }
101
+ async function sendDocumentFromPath(client, chatId, filePath, options) {
102
+ const done = await uploadFileFromPath(client, filePath, {
103
+ fileName: options?.fileName,
104
+ fileType: options?.fileType
105
+ });
106
+ return client.sendDocument({
107
+ chat_id: chatId,
108
+ file_id: done.id,
109
+ caption: options?.caption
110
+ });
111
+ }
112
+ async function sendPhotoFromBuffer(client, chatId, buffer, options) {
113
+ const done = await uploadBuffer(client, buffer, options?.fileName || "image.bin", options?.fileType);
114
+ return client.sendPhoto({
115
+ chat_id: chatId,
116
+ file_id: done.id,
117
+ caption: options?.caption
118
+ });
119
+ }
120
+ async function sendPhotoFromUrl(client, chatId, sourceUrl, options) {
121
+ const done = await uploadFromUrl(client, sourceUrl, options);
122
+ return client.sendPhoto({
123
+ chat_id: chatId,
124
+ file_id: done.id,
125
+ caption: options?.caption
126
+ });
127
+ }
128
+ async function sendVideoFromUrl(client, chatId, sourceUrl, options) {
129
+ const done = await uploadFromUrl(client, sourceUrl, {
130
+ fileName: options?.fileName,
131
+ fileType: options?.fileType
132
+ });
133
+ return client.sendVideo({
134
+ chat_id: chatId,
135
+ file_id: done.id,
136
+ caption: options?.caption,
137
+ duration: options?.duration,
138
+ width: options?.width,
139
+ height: options?.height
140
+ });
141
+ }
142
+
143
+ // src/client.ts
144
+ var DEFAULT_BASE = typeof process !== "undefined" && process.env.STARIM_API_BASE ? String(process.env.STARIM_API_BASE).replace(/\/+$/, "") : "https://api.starim.example/api/v1";
145
+ function joinUrl(base, path2) {
146
+ const b = base.replace(/\/+$/, "");
147
+ const p = path2.startsWith("/") ? path2 : `/${path2}`;
148
+ return `${b}${p}`;
149
+ }
150
+ function unwrapData(body) {
151
+ if (!body || typeof body !== "object") return body;
152
+ const b = body;
153
+ if (b.success !== true) return body;
154
+ let d = b.data;
155
+ if (d && typeof d === "object") {
156
+ const inner = d;
157
+ if (inner.success === true && "data" in inner && typeof inner.data !== "undefined" && Object.keys(inner).length <= 5) {
158
+ d = inner.data;
159
+ }
160
+ }
161
+ return d;
162
+ }
163
+ function isPlainObject(v) {
164
+ return typeof v === "object" && v !== null && !Array.isArray(v);
165
+ }
166
+ var StarIMBotClient = class {
167
+ token;
168
+ baseUrl;
169
+ timeoutMs;
170
+ debug;
171
+ /** 暴露给上层(如 StarIMBot polling 链路)以记录非致命错误 */
172
+ logger;
173
+ files;
174
+ constructor(opts) {
175
+ if (!opts.token?.trim()) {
176
+ throw new Error("StarIMBotClient: token is required");
177
+ }
178
+ this.token = opts.token.trim();
179
+ this.baseUrl = (opts.baseUrl || DEFAULT_BASE).replace(/\/+$/, "");
180
+ this.timeoutMs = opts.timeoutMs ?? 3e4;
181
+ this.debug = !!opts.debug;
182
+ this.logger = opts.logger ?? console;
183
+ this.files = {
184
+ issueUploadCredentials: (p) => this.issueUploadCredentials(p),
185
+ completeUpload: (p) => this.completeUpload(p)
186
+ };
187
+ }
188
+ log(level, msg, meta) {
189
+ if (!this.debug) return;
190
+ this.logger[level](`[StarIMBotClient] ${msg}`, meta ?? "");
191
+ }
192
+ async request(method, path2, body, opts) {
193
+ const url = joinUrl(this.baseUrl, path2);
194
+ const auth = opts?.auth !== false;
195
+ const headers = {
196
+ Accept: "application/json"
197
+ };
198
+ if (auth) {
199
+ headers.Authorization = `Bearer ${this.token}`;
200
+ }
201
+ const hasBody = body !== void 0 && method !== "GET" && method !== "HEAD";
202
+ if (hasBody) {
203
+ headers["Content-Type"] = "application/json";
204
+ }
205
+ const effectiveTimeout = opts?.timeoutMs ?? this.timeoutMs;
206
+ const ac = new AbortController();
207
+ const timer = setTimeout(() => ac.abort(), effectiveTimeout);
208
+ this.log("debug", `${method} ${path2}`);
209
+ let res;
210
+ try {
211
+ res = await fetch(url, {
212
+ method,
213
+ headers,
214
+ body: hasBody ? JSON.stringify(body) : void 0,
215
+ signal: ac.signal
216
+ });
217
+ } catch (e) {
218
+ clearTimeout(timer);
219
+ const msg = e instanceof Error ? e.message : String(e);
220
+ throw new StarIMApiError({
221
+ message: msg.includes("abort") ? "Request timeout" : msg,
222
+ statusCode: 0,
223
+ responseBody: void 0
224
+ });
225
+ } finally {
226
+ clearTimeout(timer);
227
+ }
228
+ const text = await res.text();
229
+ let json;
230
+ try {
231
+ json = text ? JSON.parse(text) : {};
232
+ } catch {
233
+ throw new StarIMApiError({
234
+ message: text.slice(0, 500) || "Invalid JSON response",
235
+ statusCode: res.status,
236
+ responseBody: text
237
+ });
238
+ }
239
+ if (!isPlainObject(json)) {
240
+ throw new StarIMApiError({
241
+ message: "Unexpected response shape",
242
+ statusCode: res.status,
243
+ responseBody: json
244
+ });
245
+ }
246
+ if (json.success === false) {
247
+ throw new StarIMApiError({
248
+ message: String(json.message || "Request failed"),
249
+ statusCode: res.status || Number(json.code) || 400,
250
+ code: json.code,
251
+ responseBody: json
252
+ });
253
+ }
254
+ if (!res.ok && json.success !== true) {
255
+ throw new StarIMApiError({
256
+ message: String(json.message || res.statusText || "HTTP error"),
257
+ statusCode: res.status,
258
+ code: json.code,
259
+ responseBody: json
260
+ });
261
+ }
262
+ if (json.success !== true) {
263
+ throw new StarIMApiError({
264
+ message: String(json.message || "Request failed"),
265
+ statusCode: res.status,
266
+ code: json.code,
267
+ responseBody: json
268
+ });
269
+ }
270
+ return unwrapData(json);
271
+ }
272
+ /** GET /bots/me */
273
+ getMe() {
274
+ return this.request("GET", "/bots/me");
275
+ }
276
+ /** GET /bots/getChat?chat_id= */
277
+ getChat(chat_id) {
278
+ const q = new URLSearchParams({ chat_id });
279
+ return this.request("GET", `/bots/getChat?${q.toString()}`);
280
+ }
281
+ getChatMember(chat_id, user_id) {
282
+ const q = new URLSearchParams({ chat_id, user_id });
283
+ return this.request("GET", `/bots/getChatMember?${q.toString()}`);
284
+ }
285
+ getChatMemberCount(chat_id) {
286
+ const q = new URLSearchParams({ chat_id });
287
+ return this.request("GET", `/bots/getChatMemberCount?${q.toString()}`);
288
+ }
289
+ getChatAdministrators(chat_id) {
290
+ const q = new URLSearchParams({ chat_id });
291
+ return this.request("GET", `/bots/getChatAdministrators?${q.toString()}`);
292
+ }
293
+ setChatTitle(chat_id, title) {
294
+ return this.request("POST", "/bots/setChatTitle", { chat_id, title });
295
+ }
296
+ setChatDescription(chat_id, description) {
297
+ return this.request("POST", "/bots/setChatDescription", { chat_id, description });
298
+ }
299
+ setChatPhoto(chat_id, photo) {
300
+ return this.request("POST", "/bots/setChatPhoto", { chat_id, photo });
301
+ }
302
+ pinChatMessage(body) {
303
+ return this.request("POST", "/bots/pinChatMessage", body);
304
+ }
305
+ unpinChatMessage(body) {
306
+ return this.request("POST", "/bots/unpinChatMessage", body);
307
+ }
308
+ unpinAllChatMessages(chat_id) {
309
+ return this.request("POST", "/bots/unpinAllChatMessages", { chat_id });
310
+ }
311
+ forwardMessage(body) {
312
+ return this.request("POST", "/bots/forwardMessage", body);
313
+ }
314
+ copyMessage(body) {
315
+ return this.request("POST", "/bots/copyMessage", body);
316
+ }
317
+ sendChatAction(chat_id, action) {
318
+ return this.request("POST", "/bots/sendChatAction", { chat_id, action });
319
+ }
320
+ getFile(file_id) {
321
+ const q = new URLSearchParams({ file_id });
322
+ return this.request("GET", `/bots/getFile?${q.toString()}`);
323
+ }
324
+ /** POST /bots/sendMessage */
325
+ sendMessage(params) {
326
+ return this.request("POST", "/bots/sendMessage", {
327
+ chat_id: params.chat_id,
328
+ text: params.text,
329
+ reply_markup: params.reply_markup
330
+ });
331
+ }
332
+ /** POST /bots/setMyCommands */
333
+ setMyCommands(params) {
334
+ return this.request("POST", "/bots/setMyCommands", {
335
+ commands: params.commands,
336
+ scope: params.scope,
337
+ language_code: params.language_code
338
+ });
339
+ }
340
+ /** GET /bots/getMyCommands */
341
+ getMyCommands(params) {
342
+ const q = new URLSearchParams();
343
+ if (params?.scope) q.set("scope", params.scope);
344
+ if (params?.language_code) q.set("language_code", params.language_code);
345
+ const suffix = q.toString() ? `?${q.toString()}` : "";
346
+ return this.request("GET", `/bots/getMyCommands${suffix}`);
347
+ }
348
+ /** POST /bots/deleteMyCommands */
349
+ deleteMyCommands(params) {
350
+ return this.request("POST", "/bots/deleteMyCommands", {
351
+ scope: params?.scope,
352
+ language_code: params?.language_code
353
+ });
354
+ }
355
+ /** POST /bots/setMyShortDescription · 设置短描述(≤120 字) */
356
+ setMyShortDescription(short_description) {
357
+ return this.request("POST", "/bots/setMyShortDescription", { short_description });
358
+ }
359
+ /** GET /bots/getMyShortDescription · 获取短描述 */
360
+ getMyShortDescription() {
361
+ return this.request("GET", "/bots/getMyShortDescription");
362
+ }
363
+ /** POST /bots/setMyDescription · 设置 Bot 主页大段「关于」文本(≤512 字) */
364
+ setMyDescription(description) {
365
+ return this.request("POST", "/bots/setMyDescription", { description });
366
+ }
367
+ /** GET /bots/getMyDescription · 获取 Bot 主页大段「关于」文本 */
368
+ getMyDescription() {
369
+ return this.request("GET", "/bots/getMyDescription");
370
+ }
371
+ setWebhook(params) {
372
+ const defaultAllowed = [
373
+ "message",
374
+ "edited_message",
375
+ "message_deleted",
376
+ "message_read",
377
+ "message_delivered",
378
+ "callback_query",
379
+ "inline_query",
380
+ "friend_request"
381
+ ];
382
+ return this.request("POST", "/bots/setWebhook", {
383
+ url: params.url,
384
+ secret_token: params.secret_token ?? "",
385
+ allowed_updates: params.allowed_updates ?? defaultAllowed,
386
+ allowed_ips: params.allowed_ips ?? []
387
+ });
388
+ }
389
+ deleteWebhook() {
390
+ return this.request("POST", "/bots/deleteWebhook", {});
391
+ }
392
+ /** GET /bots/getWebhookInfo */
393
+ getWebhookInfo() {
394
+ return this.request("GET", "/bots/getWebhookInfo");
395
+ }
396
+ /**
397
+ * POST /bots/getUpdates · 长轮询拉取 polling 模式 update。
398
+ *
399
+ * - 调本接口前请确保已 `deleteWebhook`,否则平台返回 409。
400
+ * - HTTP 客户端的超时会按 `timeout + 5s` 自动放宽,避免比服务端先断开。
401
+ *
402
+ * @example
403
+ * ```ts
404
+ * let offset = 0;
405
+ * while (true) {
406
+ * const { updates } = await client.getUpdates({ timeout: 30, offset });
407
+ * for (const u of updates) {
408
+ * console.log(u);
409
+ * if (typeof u.update_seq === 'number') offset = u.update_seq + 1;
410
+ * }
411
+ * }
412
+ * ```
413
+ */
414
+ async getUpdates(params = {}) {
415
+ const timeoutSec = Math.min(50, Math.max(0, Number(params.timeout) || 0));
416
+ const httpTimeoutMs = (timeoutSec + 5) * 1e3;
417
+ const data = await this.request(
418
+ "POST",
419
+ "/bots/getUpdates",
420
+ {
421
+ offset: params.offset ?? 0,
422
+ limit: params.limit ?? 100,
423
+ timeout: timeoutSec,
424
+ allowed_updates: params.allowed_updates ?? []
425
+ },
426
+ { timeoutMs: httpTimeoutMs }
427
+ );
428
+ return { updates: Array.isArray(data?.updates) ? data.updates : [] };
429
+ }
430
+ sendPhoto(params) {
431
+ return this.request("POST", "/bots/sendPhoto", this.buildSendMediaBody(params));
432
+ }
433
+ sendDocument(params) {
434
+ return this.request("POST", "/bots/sendDocument", this.buildSendMediaBody(params));
435
+ }
436
+ sendVideo(params) {
437
+ return this.request("POST", "/bots/sendVideo", this.buildSendMediaBody(params));
438
+ }
439
+ sendAudio(params) {
440
+ return this.request("POST", "/bots/sendAudio", this.buildSendMediaBody(params));
441
+ }
442
+ sendVoice(params) {
443
+ return this.request("POST", "/bots/sendVoice", this.buildSendMediaBody(params));
444
+ }
445
+ sendVideoNote(params) {
446
+ return this.request("POST", "/bots/sendVideoNote", this.buildSendVideoNoteBody(params));
447
+ }
448
+ sendAnimation(params) {
449
+ return this.request("POST", "/bots/sendAnimation", this.buildSendMediaBody(params));
450
+ }
451
+ sendSticker(params) {
452
+ const body = {
453
+ chat_id: params.chat_id,
454
+ file_id: params.file_id
455
+ };
456
+ const w = Number(params.width);
457
+ if (params.width != null && Number.isFinite(w) && w > 0) {
458
+ body.width = w;
459
+ }
460
+ const h = Number(params.height);
461
+ if (params.height != null && Number.isFinite(h) && h > 0) {
462
+ body.height = h;
463
+ }
464
+ if (typeof params.thumbnail_url === "string" && params.thumbnail_url.trim()) {
465
+ body.thumbnail_url = params.thumbnail_url.trim();
466
+ }
467
+ return this.request("POST", "/bots/sendSticker", body);
468
+ }
469
+ sendDice(params) {
470
+ const body = { chat_id: params.chat_id };
471
+ if (params.emoji != null && String(params.emoji).trim()) {
472
+ body.emoji = String(params.emoji).trim();
473
+ }
474
+ if (params.reply_markup !== void 0) {
475
+ body.reply_markup = params.reply_markup;
476
+ }
477
+ return this.request("POST", "/bots/sendDice", body);
478
+ }
479
+ sendPoll(params) {
480
+ const body = {
481
+ chat_id: params.chat_id,
482
+ question: params.question,
483
+ options: params.options
484
+ };
485
+ if (params.is_anonymous !== void 0) {
486
+ body.is_anonymous = params.is_anonymous;
487
+ }
488
+ if (params.type !== void 0) {
489
+ body.type = params.type;
490
+ }
491
+ if (params.correct_option_id != null) {
492
+ body.correct_option_id = params.correct_option_id;
493
+ }
494
+ if (params.reply_markup !== void 0) {
495
+ body.reply_markup = params.reply_markup;
496
+ }
497
+ return this.request("POST", "/bots/sendPoll", body);
498
+ }
499
+ sendContact(params) {
500
+ const body = {
501
+ chat_id: params.chat_id,
502
+ phone_number: params.phone_number,
503
+ first_name: params.first_name
504
+ };
505
+ if (params.last_name != null) {
506
+ body.last_name = params.last_name;
507
+ }
508
+ if (params.reply_markup !== void 0) {
509
+ body.reply_markup = params.reply_markup;
510
+ }
511
+ return this.request("POST", "/bots/sendContact", body);
512
+ }
513
+ sendMediaGroup(params) {
514
+ const body = {
515
+ chat_id: params.chat_id,
516
+ media: params.media
517
+ };
518
+ if (params.reply_markup !== void 0) {
519
+ body.reply_markup = params.reply_markup;
520
+ }
521
+ return this.request("POST", "/bots/sendMediaGroup", body);
522
+ }
523
+ buildSendMediaBody(params) {
524
+ const body = {
525
+ chat_id: params.chat_id,
526
+ file_id: params.file_id,
527
+ caption: params.caption ?? ""
528
+ };
529
+ const d = Number(params.duration);
530
+ if (params.duration != null && Number.isFinite(d) && d > 0) {
531
+ body.duration = d;
532
+ }
533
+ if (typeof params.performer === "string" && params.performer.trim()) {
534
+ body.performer = params.performer.trim();
535
+ }
536
+ if (typeof params.title === "string" && params.title.trim()) {
537
+ body.title = params.title.trim();
538
+ }
539
+ if (typeof params.thumbnail_url === "string" && params.thumbnail_url.trim()) {
540
+ body.thumbnail_url = params.thumbnail_url.trim();
541
+ }
542
+ const w = Number(params.width);
543
+ if (params.width != null && Number.isFinite(w) && w > 0) {
544
+ body.width = w;
545
+ }
546
+ const h = Number(params.height);
547
+ if (params.height != null && Number.isFinite(h) && h > 0) {
548
+ body.height = h;
549
+ }
550
+ return body;
551
+ }
552
+ buildSendVideoNoteBody(params) {
553
+ const body = this.buildSendMediaBody(params);
554
+ const durRaw = params.duration != null ? params.duration : params.length;
555
+ const d = Number(durRaw);
556
+ if (durRaw != null && Number.isFinite(d) && d > 0) {
557
+ body.duration = d;
558
+ }
559
+ return body;
560
+ }
561
+ sendLocation(params) {
562
+ return this.request("POST", "/bots/sendLocation", {
563
+ chat_id: params.chat_id,
564
+ latitude: params.latitude,
565
+ longitude: params.longitude,
566
+ name: params.name ?? "",
567
+ address: params.address ?? ""
568
+ });
569
+ }
570
+ sendVenue(params) {
571
+ const body = {
572
+ chat_id: params.chat_id,
573
+ latitude: params.latitude,
574
+ longitude: params.longitude,
575
+ title: params.title,
576
+ address: params.address ?? ""
577
+ };
578
+ if (params.reply_markup !== void 0) {
579
+ body.reply_markup = params.reply_markup;
580
+ }
581
+ return this.request("POST", "/bots/sendVenue", body);
582
+ }
583
+ editMessage(params) {
584
+ return this.request("POST", "/bots/editMessage", {
585
+ message_id: params.message_id,
586
+ text: params.text,
587
+ content: params.content,
588
+ reply_markup: params.reply_markup
589
+ });
590
+ }
591
+ editMessageReplyMarkup(params) {
592
+ return this.request("POST", "/bots/editMessageReplyMarkup", {
593
+ message_id: params.message_id,
594
+ reply_markup: params.reply_markup ?? null
595
+ });
596
+ }
597
+ answerCallbackQuery(params) {
598
+ return this.request("POST", "/bots/answerCallbackQuery", {
599
+ callback_query_id: params.callback_query_id,
600
+ text: params.text ?? "",
601
+ show_alert: params.show_alert ?? false,
602
+ url: params.url ?? "",
603
+ cache_time: params.cache_time ?? 0
604
+ });
605
+ }
606
+ answerInlineQuery(params) {
607
+ return this.request("POST", "/bots/answerInlineQuery", {
608
+ inline_query_id: params.inline_query_id,
609
+ results: params.results,
610
+ cache_time: params.cache_time ?? 300,
611
+ is_personal: params.is_personal ?? false,
612
+ next_offset: params.next_offset ?? ""
613
+ });
614
+ }
615
+ /** POST /bots/answerFriendRequest · manual 模式下处理用户发起的好友申请 */
616
+ answerFriendRequest(params) {
617
+ return this.request("POST", "/bots/answerFriendRequest", {
618
+ friendship_id: params.friendship_id,
619
+ action: params.action
620
+ });
621
+ }
622
+ /** POST /bots/setMyFriendRequestMode · 配置当前机器人好友申请处理策略 */
623
+ setMyFriendRequestMode(params) {
624
+ return this.request("POST", "/bots/setMyFriendRequestMode", {
625
+ friend_request_mode: params.friend_request_mode
626
+ });
627
+ }
628
+ /** GET /bots/getMyFriendRequestMode · 获取当前机器人好友申请处理策略 */
629
+ getMyFriendRequestMode() {
630
+ return this.request("GET", "/bots/getMyFriendRequestMode");
631
+ }
632
+ /** GET /bots/getFriendRequests · 待处理好友申请(数据库 Friendship) */
633
+ getFriendRequests() {
634
+ return this.request("GET", "/bots/getFriendRequests");
635
+ }
636
+ kickChatMember(params) {
637
+ return this.request("POST", "/bots/kickChatMember", {
638
+ chat_id: params.chat_id,
639
+ user_id: params.user_id
640
+ });
641
+ }
642
+ banChatMember(params) {
643
+ return this.request("POST", "/bots/banChatMember", {
644
+ chat_id: params.chat_id,
645
+ user_id: params.user_id,
646
+ reason: params.reason ?? ""
647
+ });
648
+ }
649
+ unbanChatMember(params) {
650
+ return this.request("POST", "/bots/unbanChatMember", {
651
+ chat_id: params.chat_id,
652
+ user_id: params.user_id
653
+ });
654
+ }
655
+ deleteMessage(params) {
656
+ return this.request("POST", "/bots/deleteMessage", {
657
+ message_id: params.message_id
658
+ });
659
+ }
660
+ issueUploadCredentials(p) {
661
+ return this.request("POST", "/bots/files/upload-credentials", {
662
+ fileName: p.fileName,
663
+ fileSize: p.fileSize,
664
+ fileType: p.fileType ?? "",
665
+ checksum: p.checksum ?? "",
666
+ metadata: p.metadata ?? {}
667
+ });
668
+ }
669
+ completeUpload(p) {
670
+ return this.request("POST", "/bots/files/complete", {
671
+ key: p.key,
672
+ fileName: p.fileName,
673
+ fileSize: p.fileSize,
674
+ fileType: p.fileType ?? "",
675
+ checksum: p.checksum ?? "",
676
+ category: p.category ?? "bot",
677
+ isPublic: p.isPublic ?? false
678
+ });
679
+ }
680
+ /**
681
+ * 校验 Token(公开接口,不强制带 Bearer,仅 body.token)。
682
+ */
683
+ verifyToken() {
684
+ return this.request("POST", "/bots/verify-token", { token: this.token }, { auth: false });
685
+ }
686
+ /** 本地路径 → 上传 S3 → sendPhoto */
687
+ sendPhotoFromFile(chatId, filePath, options) {
688
+ return sendPhotoFromPath(this, chatId, filePath, options);
689
+ }
690
+ /** 本地路径 → 上传 S3 → sendDocument */
691
+ sendDocumentFromFile(chatId, filePath, options) {
692
+ return sendDocumentFromPath(this, chatId, filePath, options);
693
+ }
694
+ };
695
+ var SIG_PREFIX = "sha256=";
696
+ var DEFAULT_TIMESTAMP_TOLERANCE_SEC = 300;
697
+ function computeWebhookSignatureV2(secret, timestampSec, rawBody) {
698
+ const ts = String(timestampSec);
699
+ const raw = typeof rawBody === "string" ? Buffer.from(rawBody, "utf8") : rawBody;
700
+ const h = crypto.createHmac("sha256", secret);
701
+ h.update(ts);
702
+ h.update(".");
703
+ h.update(raw);
704
+ return h.digest("hex");
705
+ }
706
+ function timingSafeHexEqual(a, b) {
707
+ let bufA;
708
+ let bufB;
709
+ try {
710
+ bufA = Buffer.from(a, "hex");
711
+ bufB = Buffer.from(b, "hex");
712
+ } catch {
713
+ return false;
714
+ }
715
+ if (bufA.length !== bufB.length || bufA.length === 0) return false;
716
+ return crypto.timingSafeEqual(bufA, bufB);
717
+ }
718
+ function parseSigHeader(value) {
719
+ const trimmed = value.trim();
720
+ if (!trimmed.toLowerCase().startsWith(SIG_PREFIX)) return null;
721
+ return trimmed.slice(SIG_PREFIX.length).trim();
722
+ }
723
+ function verifyWebhookSignature(secret, rawBody, signatureHeaderV2, timestampHeader, opts = {}) {
724
+ const tolerance = opts.timestampToleranceSec ?? DEFAULT_TIMESTAMP_TOLERANCE_SEC;
725
+ const nowSec = (opts.nowSec ?? (() => Math.floor(Date.now() / 1e3)))();
726
+ if (!signatureHeaderV2) {
727
+ throw new StarIMSignatureError("Missing X-StarIM-Signature-V2");
728
+ }
729
+ if (!timestampHeader) {
730
+ throw new StarIMSignatureError("Missing X-StarIM-Timestamp for V2 signature");
731
+ }
732
+ const ts = Number(timestampHeader);
733
+ if (!Number.isFinite(ts)) {
734
+ throw new StarIMSignatureError("Invalid X-StarIM-Timestamp");
735
+ }
736
+ if (Math.abs(nowSec - ts) > tolerance) {
737
+ throw new StarIMSignatureError(
738
+ `Timestamp out of tolerance window (\xB1${tolerance}s); possible replay`
739
+ );
740
+ }
741
+ const expected = parseSigHeader(signatureHeaderV2);
742
+ if (!expected) {
743
+ throw new StarIMSignatureError("Invalid X-StarIM-Signature-V2 format");
744
+ }
745
+ const actual = computeWebhookSignatureV2(secret, ts, rawBody);
746
+ if (!timingSafeHexEqual(actual, expected)) {
747
+ throw new StarIMSignatureError("V2 signature mismatch");
748
+ }
749
+ }
750
+ function headerValue(headers, name) {
751
+ const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase());
752
+ if (!key) return void 0;
753
+ const v = headers[key];
754
+ if (Array.isArray(v)) return v[0];
755
+ return v;
756
+ }
757
+ var LruDedupe = class {
758
+ constructor(maxSize) {
759
+ this.maxSize = maxSize;
760
+ }
761
+ order = [];
762
+ set = /* @__PURE__ */ new Set();
763
+ has(updateId) {
764
+ return this.set.has(updateId);
765
+ }
766
+ add(updateId) {
767
+ if (this.set.has(updateId)) return;
768
+ this.set.add(updateId);
769
+ this.order.push(updateId);
770
+ while (this.order.length > this.maxSize) {
771
+ const rm = this.order.shift();
772
+ if (rm) this.set.delete(rm);
773
+ }
774
+ }
775
+ };
776
+ function getRawBody(req) {
777
+ if (Buffer.isBuffer(req.rawBody)) return req.rawBody;
778
+ if (Buffer.isBuffer(req.body)) return req.body;
779
+ throw new StarIMSignatureError(
780
+ 'Raw JSON body required for HMAC: use express.raw({ type: "*/*" }) or express.json({ verify: (req, buf) => { (req as any).rawBody = buf } })'
781
+ );
782
+ }
783
+ function processWebhook(req, opts) {
784
+ const raw = getRawBody(req);
785
+ const sigV2 = headerValue(req.headers, "x-starim-signature-v2");
786
+ const ts = headerValue(req.headers, "x-starim-timestamp");
787
+ verifyWebhookSignature(opts.secretToken, raw, sigV2, ts, opts.verify);
788
+ let update;
789
+ try {
790
+ update = JSON.parse(raw.toString("utf8"));
791
+ } catch {
792
+ throw new StarIMSignatureError("Invalid JSON body");
793
+ }
794
+ if (!update || typeof update !== "object" || typeof update.update_id !== "string") {
795
+ throw new StarIMSignatureError("Invalid Update payload");
796
+ }
797
+ const headerId = headerValue(req.headers, "x-starim-update-id");
798
+ if (headerId && headerId !== update.update_id) {
799
+ throw new StarIMSignatureError("X-StarIM-Update-Id does not match body.update_id");
800
+ }
801
+ let duplicate = false;
802
+ if (opts.dedupe) {
803
+ if (opts.dedupe.has(update.update_id)) {
804
+ duplicate = true;
805
+ } else {
806
+ opts.dedupe.add(update.update_id);
807
+ }
808
+ }
809
+ return { update, duplicate };
810
+ }
811
+ function webhookMiddleware(opts) {
812
+ return async (req, res, next) => {
813
+ try {
814
+ if (req.method && req.method !== "POST") {
815
+ res.statusCode = 405;
816
+ res.end("Method Not Allowed");
817
+ return;
818
+ }
819
+ const { update, duplicate } = processWebhook(req, {
820
+ secretToken: opts.secretToken,
821
+ dedupe: opts.dedupe,
822
+ verify: opts.verify
823
+ });
824
+ if (duplicate && !opts.invokeOnDuplicate) {
825
+ res.statusCode = 200;
826
+ res.end("ok");
827
+ return;
828
+ }
829
+ await opts.onUpdate(update, { rawBody: getRawBody(req), duplicate });
830
+ res.statusCode = 200;
831
+ res.end("ok");
832
+ } catch (e) {
833
+ if (e instanceof StarIMSignatureError) {
834
+ res.statusCode = 401;
835
+ res.end("unauthorized");
836
+ return;
837
+ }
838
+ if (typeof next === "function") {
839
+ next(e);
840
+ return;
841
+ }
842
+ res.statusCode = 500;
843
+ res.end("error");
844
+ }
845
+ };
846
+ }
847
+
848
+ // src/bot.ts
849
+ function toNodeHeaders(h) {
850
+ const out = {};
851
+ for (const [k, v] of Object.entries(h)) {
852
+ out[k] = v;
853
+ }
854
+ return out;
855
+ }
856
+ async function readBody(req) {
857
+ const chunks = [];
858
+ for await (const chunk of req) {
859
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk));
860
+ }
861
+ return Buffer.concat(chunks);
862
+ }
863
+ var StarIMBot = class {
864
+ client;
865
+ secretToken;
866
+ dedupe;
867
+ handlers = /* @__PURE__ */ new Map();
868
+ commands = /* @__PURE__ */ new Map();
869
+ pollingActive = false;
870
+ pollingOffset = 0;
871
+ constructor(opts) {
872
+ this.secretToken = opts.secretToken;
873
+ this.client = new StarIMBotClient({
874
+ token: opts.token,
875
+ baseUrl: opts.baseUrl,
876
+ timeoutMs: opts.timeoutMs,
877
+ debug: opts.debug,
878
+ logger: opts.logger
879
+ });
880
+ this.dedupe = new LruDedupe(opts.dedupeSize ?? 1024);
881
+ }
882
+ on(type, handler) {
883
+ const key = type;
884
+ const list = this.handlers.get(key) ?? [];
885
+ list.push(handler);
886
+ this.handlers.set(key, list);
887
+ return this;
888
+ }
889
+ /**
890
+ * 仅匹配文本消息中以 `/name` 开头的命令(大小写不敏感)。
891
+ */
892
+ command(name, handler) {
893
+ const n = name.replace(/^\//, "").toLowerCase();
894
+ const list = this.commands.get(n) ?? [];
895
+ list.push(handler);
896
+ this.commands.set(n, list);
897
+ return this;
898
+ }
899
+ async dispatch(ctx) {
900
+ const { update } = ctx;
901
+ const type = update.type;
902
+ if (type === "message" && ctx.message.text) {
903
+ const text = ctx.message.text.trim();
904
+ const m = /^\/(\w+)/i.exec(text);
905
+ if (m) {
906
+ const cmd = m[1].toLowerCase();
907
+ const cmdHandlers = this.commands.get(cmd);
908
+ if (cmdHandlers?.length) {
909
+ for (const h of cmdHandlers) {
910
+ await h(ctx);
911
+ }
912
+ return;
913
+ }
914
+ }
915
+ }
916
+ const specific = this.handlers.get(type) ?? [];
917
+ for (const h of specific) {
918
+ await h(ctx);
919
+ }
920
+ const star = this.handlers.get("*") ?? [];
921
+ for (const h of star) {
922
+ await h(ctx);
923
+ }
924
+ }
925
+ /**
926
+ * Express / Connect 中间件:先挂载 express.raw(例如 type: application/json),再使用本回调。
927
+ */
928
+ webhookCallback() {
929
+ return async (req, res, next) => {
930
+ try {
931
+ if (req.method && req.method !== "POST") {
932
+ res.statusCode = 405;
933
+ res.end("Method Not Allowed");
934
+ return;
935
+ }
936
+ const { update, duplicate } = processWebhook(req, {
937
+ secretToken: this.secretToken,
938
+ dedupe: this.dedupe
939
+ });
940
+ if (duplicate) {
941
+ res.statusCode = 200;
942
+ res.end("ok");
943
+ return;
944
+ }
945
+ const ctx = this.createContext(update, getRaw(req), duplicate);
946
+ await this.dispatch(ctx);
947
+ res.statusCode = 200;
948
+ res.end("ok");
949
+ } catch (e) {
950
+ if (e instanceof StarIMSignatureError) {
951
+ res.statusCode = 401;
952
+ res.end("unauthorized");
953
+ return;
954
+ }
955
+ if (typeof next === "function") {
956
+ next(e);
957
+ return;
958
+ }
959
+ res.statusCode = 500;
960
+ res.end("error");
961
+ }
962
+ };
963
+ }
964
+ /**
965
+ * 长轮询模式:循环调用 getUpdates,把 update 喂给与 Webhook 相同的 dispatch 链路。
966
+ *
967
+ * 适用场景:
968
+ * - 没有公网 IP / HTTPS 证书的本机开发;
969
+ * - CI / 测试环境快速接入。
970
+ *
971
+ * 使用前请先 `await client.deleteWebhook()`,否则平台返回 409。
972
+ * 内部维护 offset、自动捕获错误并指数退避重试;调用 `stopPolling()` 优雅退出。
973
+ *
974
+ * @example
975
+ * ```ts
976
+ * await bot.client.deleteWebhook();
977
+ * await bot.startPolling({ timeout: 30 });
978
+ * ```
979
+ */
980
+ async startPolling(options = {}) {
981
+ if (this.pollingActive) return;
982
+ this.pollingActive = true;
983
+ this.pollingOffset = 0;
984
+ const timeoutSec = Math.min(50, Math.max(0, options.timeout ?? 30));
985
+ const limit = Math.min(100, Math.max(1, options.limit ?? 100));
986
+ const allowed = options.allowedUpdates ?? [];
987
+ const maxBackoff = Math.max(1, options.maxBackoffSec ?? 30);
988
+ let consecutiveErrors = 0;
989
+ while (this.pollingActive) {
990
+ try {
991
+ const { updates } = await this.client.getUpdates({
992
+ offset: this.pollingOffset,
993
+ limit,
994
+ timeout: timeoutSec,
995
+ allowed_updates: allowed
996
+ });
997
+ consecutiveErrors = 0;
998
+ for (const u of updates) {
999
+ if (typeof u.update_seq === "number") {
1000
+ this.pollingOffset = Math.max(this.pollingOffset, u.update_seq + 1);
1001
+ }
1002
+ const uid = String(u.update_id);
1003
+ if (this.dedupe.has(uid)) continue;
1004
+ this.dedupe.add(uid);
1005
+ const ctx = this.createContext(u, Buffer.alloc(0), false);
1006
+ try {
1007
+ await this.dispatch(ctx);
1008
+ } catch (handlerErr) {
1009
+ try {
1010
+ this.client.logger?.error?.("[StarIMBot.polling] handler error", { error: handlerErr?.message });
1011
+ } catch {
1012
+ }
1013
+ }
1014
+ }
1015
+ } catch (err) {
1016
+ consecutiveErrors += 1;
1017
+ const delay = Math.min(maxBackoff, Math.pow(2, Math.min(consecutiveErrors, 6))) * 1e3;
1018
+ try {
1019
+ this.client.logger?.warn?.("[StarIMBot.polling] getUpdates failed, backing off", {
1020
+ error: err?.message,
1021
+ delayMs: delay
1022
+ });
1023
+ } catch {
1024
+ }
1025
+ await new Promise((r) => setTimeout(r, delay));
1026
+ }
1027
+ }
1028
+ }
1029
+ /** 停止 startPolling 的循环(不会立刻打断当前 long-poll,等当次 RPC 返回后退出) */
1030
+ stopPolling() {
1031
+ this.pollingActive = false;
1032
+ }
1033
+ /**
1034
+ * 内置 HTTP 服务,便于快速起 Webhook(生产环境建议使用反向代理 + TLS)。
1035
+ */
1036
+ start(options) {
1037
+ const webhookPath = options.path ?? "/webhook";
1038
+ const server = http.createServer(async (req, res) => {
1039
+ try {
1040
+ if (req.method !== "POST" || req.url?.split("?")[0] !== webhookPath) {
1041
+ res.statusCode = 404;
1042
+ res.end();
1043
+ return;
1044
+ }
1045
+ const raw = await readBody(req);
1046
+ const wreq = {
1047
+ method: req.method,
1048
+ headers: toNodeHeaders(req.headers),
1049
+ body: raw,
1050
+ rawBody: raw
1051
+ };
1052
+ const { update, duplicate } = processWebhook(wreq, {
1053
+ secretToken: this.secretToken,
1054
+ dedupe: this.dedupe
1055
+ });
1056
+ if (duplicate) {
1057
+ res.statusCode = 200;
1058
+ res.end("ok");
1059
+ return;
1060
+ }
1061
+ const ctx = this.createContext(update, raw, duplicate);
1062
+ await this.dispatch(ctx);
1063
+ res.statusCode = 200;
1064
+ res.end("ok");
1065
+ } catch (e) {
1066
+ if (e instanceof StarIMSignatureError) {
1067
+ res.statusCode = 401;
1068
+ res.end("unauthorized");
1069
+ return;
1070
+ }
1071
+ res.statusCode = 500;
1072
+ res.end("error");
1073
+ }
1074
+ });
1075
+ return new Promise((resolve, reject) => {
1076
+ server.listen(options.port, options.host ?? "0.0.0.0", () => resolve(server));
1077
+ server.on("error", reject);
1078
+ });
1079
+ }
1080
+ createContext(update, rawBody, duplicate) {
1081
+ const flat = update;
1082
+ if (update.type === "my_chat_member" && !update.my_chat_member && flat.chat && flat.from) {
1083
+ update.my_chat_member = {
1084
+ chat: flat.chat,
1085
+ from: flat.from,
1086
+ date: typeof update.date === "number" ? update.date : Math.floor(Date.now() / 1e3),
1087
+ old_chat_member: flat.old_chat_member,
1088
+ new_chat_member: flat.new_chat_member
1089
+ };
1090
+ }
1091
+ if (update.type === "chat_member" && !update.chat_member && flat.chat && flat.from) {
1092
+ update.chat_member = {
1093
+ chat: flat.chat,
1094
+ from: flat.from,
1095
+ date: typeof update.date === "number" ? update.date : Math.floor(Date.now() / 1e3),
1096
+ old_chat_member: flat.old_chat_member,
1097
+ new_chat_member: flat.new_chat_member
1098
+ };
1099
+ }
1100
+ if (update.type === "chat_join_request" && !update.chat_join_request && flat.chat && flat.from) {
1101
+ update.chat_join_request = {
1102
+ chat: flat.chat,
1103
+ from: flat.from,
1104
+ date: typeof update.date === "number" ? update.date : Math.floor(Date.now() / 1e3),
1105
+ user_chat_id: flat.user_chat_id ?? "",
1106
+ bio: flat.bio
1107
+ };
1108
+ }
1109
+ const callbackQuery = update.callback_query;
1110
+ const inlineQuery = update.inline_query;
1111
+ const friendRequest = update.friend_request;
1112
+ const myChatMember = update.my_chat_member;
1113
+ const chatMember = update.chat_member;
1114
+ const chatJoinRequest = update.chat_join_request;
1115
+ const memberChat = myChatMember?.chat ?? chatMember?.chat ?? chatJoinRequest?.chat;
1116
+ const message = update.message ?? callbackQuery?.message ?? {
1117
+ message_id: "",
1118
+ chat: memberChat ?? {
1119
+ id: inlineQuery?.from?.id != null ? String(inlineQuery.from.id) : friendRequest?.from?.id != null ? String(friendRequest.from.id) : "",
1120
+ type: "private"
1121
+ },
1122
+ date: Math.floor(Date.now() / 1e3)
1123
+ };
1124
+ const chat = message.chat;
1125
+ return {
1126
+ update,
1127
+ rawBody,
1128
+ duplicate,
1129
+ client: this.client,
1130
+ message,
1131
+ chat,
1132
+ callbackQuery,
1133
+ inlineQuery,
1134
+ friendRequest,
1135
+ myChatMember,
1136
+ chatMember,
1137
+ chatJoinRequest,
1138
+ reply: (text, options) => this.client.sendMessage({
1139
+ chat_id: String(chat.id),
1140
+ text,
1141
+ reply_markup: options?.reply_markup
1142
+ }),
1143
+ answerCallback: (params = {}) => {
1144
+ if (!callbackQuery?.id) {
1145
+ return Promise.reject(new Error("answerCallback: current update is not callback_query"));
1146
+ }
1147
+ return this.client.answerCallbackQuery({
1148
+ callback_query_id: callbackQuery.id,
1149
+ ...params
1150
+ });
1151
+ },
1152
+ answerInlineQuery: (params) => {
1153
+ if (!inlineQuery?.id) {
1154
+ return Promise.reject(new Error("answerInlineQuery: current update is not inline_query"));
1155
+ }
1156
+ return this.client.answerInlineQuery({
1157
+ inline_query_id: inlineQuery.id,
1158
+ ...params
1159
+ });
1160
+ }
1161
+ };
1162
+ }
1163
+ };
1164
+ function getRaw(req) {
1165
+ if (Buffer.isBuffer(req.rawBody)) return req.rawBody;
1166
+ if (Buffer.isBuffer(req.body)) return req.body;
1167
+ throw new Error("webhookCallback: missing raw body");
1168
+ }
1169
+
1170
+ exports.DEFAULT_TIMESTAMP_TOLERANCE_SEC = DEFAULT_TIMESTAMP_TOLERANCE_SEC;
1171
+ exports.LruDedupe = LruDedupe;
1172
+ exports.StarIMApiError = StarIMApiError;
1173
+ exports.StarIMBot = StarIMBot;
1174
+ exports.StarIMBotClient = StarIMBotClient;
1175
+ exports.StarIMSignatureError = StarIMSignatureError;
1176
+ exports.computeWebhookSignatureV2 = computeWebhookSignatureV2;
1177
+ exports.processWebhook = processWebhook;
1178
+ exports.sendDocumentFromPath = sendDocumentFromPath;
1179
+ exports.sendPhotoFromBuffer = sendPhotoFromBuffer;
1180
+ exports.sendPhotoFromPath = sendPhotoFromPath;
1181
+ exports.sendPhotoFromUrl = sendPhotoFromUrl;
1182
+ exports.sendVideoFromUrl = sendVideoFromUrl;
1183
+ exports.uploadFileFromPath = uploadFileFromPath;
1184
+ exports.uploadFromUrl = uploadFromUrl;
1185
+ exports.verifyWebhookSignature = verifyWebhookSignature;
1186
+ exports.webhookMiddleware = webhookMiddleware;
1187
+ //# sourceMappingURL=index.cjs.map
1188
+ //# sourceMappingURL=index.cjs.map