mifistix-bot 1.0.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/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # 🤖 mifistix-bot
2
+
3
+ Официальный **Node.js SDK** для работы с сервером **Mifistix Bot**.
4
+
5
+ Поддерживает **Long Polling (`getUpdates`)**, отправку сообщений, **Inline-клавиатуры**, обработку нажатий по кнопкам (**Callback Query**), автоматический **Heartbeat 30с** и Graceful Shutdown по Ctrl+C.
6
+
7
+ ---
8
+
9
+ ## 🚀 Быстрый старт
10
+
11
+ ### Пример с меню и Inline-клавиатурой
12
+
13
+ ```javascript
14
+ const { BotClient } = require('mifistix-bot'); // или require('./modules/sdk/mifistix-bot-sdk')
15
+
16
+ const bot = new BotClient({
17
+ token: 'ВАШ_ТОКЕН_БОТА',
18
+ });
19
+
20
+ // Клавиатура главного меню
21
+ function mainMenuKeyboard() {
22
+ return {
23
+ inline_keyboard: [
24
+ [{ text: '⚙️ Настройки', callback_data: 'menu:settings' }],
25
+ [{ text: '📊 Статистика', callback_data: 'menu:stats' }],
26
+ ],
27
+ };
28
+ }
29
+
30
+ // Клавиатура настроек
31
+ function settingsKeyboard() {
32
+ return {
33
+ inline_keyboard: [
34
+ [{ text: '🔔 Уведомления: Вкл', callback_data: 'toggle:notify' }],
35
+ [{ text: '⬅️ Назад', callback_data: 'menu:main' }],
36
+ ],
37
+ };
38
+ }
39
+
40
+ // Обработка кликов по кнопкам (callback_query)
41
+ bot.callback_query_handler((call) => {
42
+ const [action, param] = (call.data || '').split(':');
43
+
44
+ if (action === 'menu') {
45
+ if (param === 'settings') {
46
+ bot.editMessageText('⚙️ Настройки бота:', call.message.chat.id, call.message.message_id, {
47
+ reply_markup: settingsKeyboard(),
48
+ });
49
+ } else {
50
+ bot.editMessageText('🏠 Главное меню:', call.message.chat.id, call.message.message_id, {
51
+ reply_markup: mainMenuKeyboard(),
52
+ });
53
+ }
54
+ bot.answerCallbackQuery(call.id);
55
+ }
56
+ });
57
+
58
+ // Запуск бота
59
+ bot.start();
60
+ ```
61
+
62
+ ---
63
+
64
+ ## 💡 Полноценный Bot API
65
+
66
+ SDK предоставляет Telegram-совместимые методы работы с сообщениями:
67
+
68
+ - **`bot.sendMessage(chat_id, text, reply_markup)`** — отправка сообщения
69
+ - **`bot.editMessageText(text, chat_id, message_id, reply_markup)`** — редактирование сообщения
70
+ - **`bot.answerCallbackQuery(callback_query_id, options)`** — подтверждение нажатия кнопки
71
+ - **`bot.callback_query_handler(filterFn, handlerFn)`** — подписка на клики клавиатуры
72
+
73
+ ---
74
+
75
+ ## 📄 Лицензия
76
+
77
+ MIT © Mifistix
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "mifistix-bot",
3
+ "version": "1.0.0",
4
+ "description": "SDK for Mifistix Bot Server",
5
+ "main": "src/index.js",
6
+ "type": "commonjs",
7
+ "scripts": {
8
+ "test": "node --test"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "license": "MIT"
18
+ }
package/src/client.js ADDED
@@ -0,0 +1,486 @@
1
+ const http = require('node:http');
2
+ const https = require('node:https');
3
+
4
+ function requestJson(url, options = {}, bodyData = null) {
5
+ return new Promise((resolve, reject) => {
6
+ const parsedUrl = new URL(url);
7
+ const transport = parsedUrl.protocol === 'https:' ? https : http;
8
+
9
+ const reqOptions = {
10
+ hostname: parsedUrl.hostname,
11
+ port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
12
+ path: parsedUrl.pathname + parsedUrl.search,
13
+ method: options.method || 'GET',
14
+ headers: {
15
+ 'Content-Type': 'application/json',
16
+ 'User-Agent': 'mifistix-bot-sdk/1.0.0',
17
+ ...(options.headers || {}),
18
+ },
19
+ };
20
+
21
+ const req = transport.request(reqOptions, (res) => {
22
+ let chunks = '';
23
+ res.on('data', (chunk) => (chunks += chunk));
24
+ res.on('end', () => {
25
+ let json = null;
26
+ try {
27
+ json = chunks ? JSON.parse(chunks) : null;
28
+ } catch {
29
+ json = { error: chunks };
30
+ }
31
+ if (res.statusCode >= 200 && res.statusCode < 300) {
32
+ resolve(json);
33
+ } else {
34
+ const err = new Error(json?.error || json?.message || `HTTP ${res.statusCode}`);
35
+ err.status = res.statusCode;
36
+ err.data = json;
37
+ reject(err);
38
+ }
39
+ });
40
+ });
41
+
42
+ req.on('error', (err) => reject(err));
43
+
44
+ if (bodyData) {
45
+ req.write(JSON.stringify(bodyData));
46
+ }
47
+
48
+ req.end();
49
+ });
50
+ }
51
+
52
+ class BotClient {
53
+ constructor(options = {}) {
54
+ const token = typeof options === 'string' ? options : options.token;
55
+ if (!token || typeof token !== 'string') {
56
+ throw new Error('[mifistix-bot] Токен бота обязателен');
57
+ }
58
+
59
+ this.token = token.trim();
60
+ // The platform address belongs to the SDK configuration, not bot code.
61
+ this.serverUrl = (options.serverUrl || process.env.MIFISTIX_SERVER_URL || 'https://bots-api.mifistix.com.pl').replace(/\/+$/, '');
62
+ this.heartbeatIntervalMs = options.heartbeatIntervalMs || 30_000;
63
+ this.pollIntervalMs = options.pollIntervalMs || 5_000;
64
+ this.autoRunning = options.autoRunning !== false;
65
+ this.enablePolling = options.enablePolling !== false;
66
+ this.onStatusChange = options.onStatusChange || null;
67
+ this.onError = options.onError || ((err) => console.error('[mifistix-bot] Ошибка:', err.message));
68
+
69
+ this.bot = null;
70
+ this.botId = null;
71
+ this.accountId = null;
72
+ this.currentStatus = null;
73
+ this.heartbeatTimer = null;
74
+ this.pollTimer = null;
75
+ this.started = false;
76
+ this.polling = false;
77
+ this.updateOffset = 0;
78
+
79
+ this.messageHandlers = [];
80
+ this.commandHandlers = new Map();
81
+ this.callbackHandlers = [];
82
+ this.sigintHandler = null;
83
+ this.sigtermHandler = null;
84
+ }
85
+
86
+ #request(url, options = {}, bodyData = null) {
87
+ if (!this.accountId) {
88
+ throw new Error('[mifistix-bot] SDK не получил ключ accountId');
89
+ }
90
+ return requestJson(url, {
91
+ ...options,
92
+ headers: {
93
+ ...(options.headers || {}),
94
+ 'x-mifistix-account-id': this.accountId,
95
+ 'x-mifistix-bot-token': this.token,
96
+ },
97
+ }, bodyData);
98
+ }
99
+
100
+ async start() {
101
+ if (this.started) return this;
102
+
103
+ console.log('[mifistix-bot] Подключение к серверу:', this.serverUrl);
104
+
105
+ // 1. Авторизация и проверка токена
106
+ let res;
107
+ try {
108
+ res = await requestJson(`${this.serverUrl}/api/bots/login`, { method: 'POST' }, { token: this.token });
109
+ } catch (error) {
110
+ throw new Error(`[mifistix-bot] Ошибка авторизации: ${error.message}`);
111
+ }
112
+ if (!res?.id || !res?.accountId) {
113
+ throw new Error('[mifistix-bot] Сервер не вернул botId или accountId');
114
+ }
115
+ this.bot = res;
116
+ this.botId = res.id;
117
+ this.accountId = String(res.accountId);
118
+ this.currentStatus = res.status || 'running';
119
+
120
+ console.log(`[mifistix-bot] Успешный вход! Бот: "${this.bot.name || this.bot.username || this.botId}" [id: ${this.botId}]`);
121
+
122
+ // 2. Авто-перевод в статус running
123
+ if (this.autoRunning && this.currentStatus !== 'running') {
124
+ await this.setStatus('running');
125
+ }
126
+
127
+ // 3. Запуск Heartbeat (каждые 30 секунд)
128
+ this.heartbeatTimer = setInterval(() => this.#sendHeartbeat(), this.heartbeatIntervalMs);
129
+ this.heartbeatTimer.unref?.();
130
+
131
+ // 4. Запуск опроса статуса на сервере (каждые 5 секунд)
132
+ this.pollTimer = setInterval(() => this.#pollStatus(), this.pollIntervalMs);
133
+ this.pollTimer.unref?.();
134
+
135
+ // 5. Запуск Long Polling для getUpdates
136
+ // The polling loop checks `started` immediately, so set it before starting.
137
+ this.started = true;
138
+
139
+ if (this.enablePolling) {
140
+ this.polling = true;
141
+ this.#pollUpdatesLoop().catch((err) => {
142
+ if (this.onError) this.onError(err);
143
+ });
144
+ }
145
+
146
+ // 6. Обработка сигналов завершения работы (Ctrl+C)
147
+ this.#setupGracefulShutdown();
148
+
149
+ return this;
150
+ }
151
+
152
+ async setStatus(status) {
153
+ if (!this.botId) return;
154
+ const url = `${this.serverUrl}/api/bots/${this.botId}/status`;
155
+ const res = await this.#request(url, { method: 'PATCH' }, { status });
156
+ const oldStatus = this.currentStatus;
157
+ this.currentStatus = res?.bot ? res.bot.status : (res?.status || status);
158
+
159
+ if (oldStatus !== this.currentStatus) {
160
+ console.log(`[mifistix-bot] Статус изменён: ${oldStatus || '—'} ➔ ${this.currentStatus}`);
161
+ if (this.onStatusChange) {
162
+ this.onStatusChange(this.currentStatus, oldStatus);
163
+ }
164
+ }
165
+
166
+ return this.currentStatus;
167
+ }
168
+
169
+ /* ---------------- Telegram-compatible Bot API Methods ---------------- */
170
+
171
+ async sendMessage(chat_id, text, reply_markup = null) {
172
+ if (typeof chat_id === 'object' && chat_id !== null) {
173
+ ({ chat_id, text, reply_markup = null } = chat_id);
174
+ }
175
+ const url = `${this.serverUrl}/bot${this.token}/sendMessage`;
176
+ const body = {
177
+ chat_id,
178
+ text,
179
+ reply_markup: typeof reply_markup === 'object' ? reply_markup : null,
180
+ };
181
+
182
+ try {
183
+ const res = await this.#request(url, { method: 'POST' }, body);
184
+ return res?.result || res;
185
+ } catch {
186
+ const fallbackUrl = `${this.serverUrl}/api/bot/${encodeURIComponent(this.token)}/sendMessage`;
187
+ const res = await this.#request(fallbackUrl, { method: 'POST' }, body);
188
+ return res?.result || res;
189
+ }
190
+ }
191
+
192
+ async editMessageText(text, chat_id, message_id, reply_markup = null) {
193
+ // В телеграм формате: edit_message_text(text, chat_id, message_id, reply_markup)
194
+ let cId = chat_id;
195
+ let mId = message_id;
196
+ let markup = reply_markup;
197
+
198
+ // Если аргументы переданы объектом { chat_id, message_id, text, reply_markup }
199
+ if (typeof text === 'object' && text !== null) {
200
+ cId = text.chat_id;
201
+ mId = text.message_id;
202
+ markup = text.reply_markup;
203
+ text = text.text;
204
+ }
205
+
206
+ const url = `${this.serverUrl}/bot${this.token}/editMessageText`;
207
+ const body = {
208
+ chat_id: cId,
209
+ message_id: mId,
210
+ text,
211
+ reply_markup: markup || null,
212
+ };
213
+
214
+ try {
215
+ const res = await this.#request(url, { method: 'POST' }, body);
216
+ return res?.result || res;
217
+ } catch {
218
+ const fallbackUrl = `${this.serverUrl}/api/bot/${encodeURIComponent(this.token)}/editMessageText`;
219
+ const res = await this.#request(fallbackUrl, { method: 'POST' }, body);
220
+ return res?.result || res;
221
+ }
222
+ }
223
+
224
+ // Alias python-style edit_message_text
225
+ async edit_message_text(text, chat_id, message_id, reply_markup = null) {
226
+ return this.editMessageText(text, chat_id, message_id, reply_markup);
227
+ }
228
+
229
+ // Telegram-style alias.
230
+ async send_message(chat_id, text, reply_markup = null) {
231
+ return this.sendMessage(chat_id, text, reply_markup);
232
+ }
233
+
234
+ async getMe() {
235
+ const url = `${this.serverUrl}/bot${this.token}/getMe`;
236
+ try {
237
+ const res = await this.#request(url);
238
+ return res?.result || res;
239
+ } catch {
240
+ const fallbackUrl = `${this.serverUrl}/api/bot/${encodeURIComponent(this.token)}/getMe`;
241
+ const res = await this.#request(fallbackUrl);
242
+ return res?.result || res;
243
+ }
244
+ }
245
+
246
+ async answerCallbackQuery(callback_query_id, options = {}) {
247
+ let cbId = callback_query_id;
248
+ let text = options.text || '';
249
+ let showAlert = options.show_alert || options.showAlert || false;
250
+
251
+ if (typeof callback_query_id === 'object' && callback_query_id !== null) {
252
+ cbId = callback_query_id.id || callback_query_id.callback_query_id;
253
+ text = callback_query_id.text || '';
254
+ showAlert = callback_query_id.show_alert || false;
255
+ }
256
+
257
+ const url = `${this.serverUrl}/bot${this.token}/answerCallbackQuery`;
258
+ const body = {
259
+ callback_query_id: cbId,
260
+ text,
261
+ show_alert: showAlert,
262
+ };
263
+
264
+ try {
265
+ const res = await this.#request(url, { method: 'POST' }, body);
266
+ return res?.result || res;
267
+ } catch {
268
+ const fallbackUrl = `${this.serverUrl}/api/bot/${encodeURIComponent(this.token)}/answerCallbackQuery`;
269
+ const res = await this.#request(fallbackUrl, { method: 'POST' }, body);
270
+ return res?.result || res;
271
+ }
272
+ }
273
+
274
+ // Alias python-style answer_callback_query
275
+ async answer_callback_query(callback_query_id, text = '', show_alert = false) {
276
+ return this.answerCallbackQuery(callback_query_id, { text, show_alert });
277
+ }
278
+
279
+ /* ---------------- Event Handlers ---------------- */
280
+
281
+ onMessage(handler) {
282
+ if (typeof handler === 'function') {
283
+ this.messageHandlers.push(handler);
284
+ }
285
+ return this;
286
+ }
287
+
288
+ command(command, handler) {
289
+ const name = String(command || '').replace(/^\//, '').trim().toLowerCase();
290
+ if (!name || typeof handler !== 'function') {
291
+ throw new TypeError('command(name, handler) requires a command name and handler');
292
+ }
293
+ const handlers = this.commandHandlers.get(name) || [];
294
+ handlers.push(handler);
295
+ this.commandHandlers.set(name, handlers);
296
+ return this;
297
+ }
298
+
299
+ onCallbackQuery(handler) {
300
+ if (typeof handler === 'function') {
301
+ this.callbackHandlers.push({ filter: () => true, handler });
302
+ }
303
+ return this;
304
+ }
305
+
306
+ // Telegram-style callback_query_handler(func, handler)
307
+ callback_query_handler(func, handler) {
308
+ let filter = () => true;
309
+ let fn = handler;
310
+
311
+ if (typeof func === 'function' && typeof handler === 'function') {
312
+ filter = func;
313
+ fn = handler;
314
+ } else if (typeof func === 'function' && !handler) {
315
+ fn = func;
316
+ } else if (typeof func === 'string' && typeof handler === 'function') {
317
+ filter = (call) => call.data === func || call.data?.startsWith(func);
318
+ fn = handler;
319
+ }
320
+
321
+ if (fn) {
322
+ this.callbackHandlers.push({ filter, handler: fn });
323
+ }
324
+ return this;
325
+ }
326
+
327
+ on(event, handler) {
328
+ if (event === 'message') return this.onMessage(handler);
329
+ if (event === 'callback_query') return this.onCallbackQuery(handler);
330
+ return this;
331
+ }
332
+
333
+ /* ---------------- Internal Polling & Heartbeat ---------------- */
334
+
335
+ async #pollUpdatesLoop() {
336
+ while (this.polling && this.started) {
337
+ try {
338
+ const url = `${this.serverUrl}/bot${this.token}/getUpdates?offset=${this.updateOffset}&timeout=20`;
339
+ let res;
340
+ try {
341
+ res = await this.#request(url);
342
+ } catch {
343
+ const fallbackUrl = `${this.serverUrl}/api/bot/${encodeURIComponent(this.token)}/getUpdates?offset=${this.updateOffset}&timeout=20`;
344
+ res = await this.#request(fallbackUrl);
345
+ }
346
+
347
+ const updates = res?.result || [];
348
+
349
+ for (const update of updates) {
350
+ if (update.update_id >= this.updateOffset) {
351
+ this.updateOffset = update.update_id + 1;
352
+ }
353
+
354
+ if (update.message) {
355
+ await this.#handleMessage(update.message);
356
+ }
357
+ if (update.callback_query) {
358
+ await this.#handleCallbackQuery(update.callback_query);
359
+ }
360
+ }
361
+ } catch (err) {
362
+ // Пауза 2с при ошибке сети
363
+ await new Promise((r) => setTimeout(r, 2000));
364
+ }
365
+ }
366
+ }
367
+
368
+ async #handleMessage(message) {
369
+ const command = String(message.text || '').trim().match(/^\/([^\s@]+)(?:@[^\s]+)?(?:\s|$)/);
370
+ if (command) {
371
+ const handlers = this.commandHandlers.get(command[1].toLowerCase()) || [];
372
+ for (const handler of handlers) {
373
+ try {
374
+ await handler(message);
375
+ } catch (err) {
376
+ if (this.onError) this.onError(err);
377
+ }
378
+ }
379
+ }
380
+
381
+ for (const h of this.messageHandlers) {
382
+ try {
383
+ await h(message);
384
+ } catch (err) {
385
+ if (this.onError) this.onError(err);
386
+ }
387
+ }
388
+ }
389
+
390
+ async #handleCallbackQuery(call) {
391
+ for (const item of this.callbackHandlers) {
392
+ try {
393
+ if (item.filter(call)) {
394
+ await item.handler(call);
395
+ }
396
+ } catch (err) {
397
+ if (this.onError) this.onError(err);
398
+ }
399
+ }
400
+ }
401
+
402
+ async #sendHeartbeat() {
403
+ try {
404
+ if (!this.botId || !this.started) return;
405
+ await this.#request(`${this.serverUrl}/api/bots/${this.botId}/heartbeat`, { method: 'POST' });
406
+ console.log(`[mifistix-bot] 💓 Heartbeat отправлен (статус: ${this.currentStatus})`);
407
+ } catch (err) {
408
+ if (this.onError) this.onError(err);
409
+ }
410
+ }
411
+
412
+ async #pollStatus() {
413
+ try {
414
+ if (!this.botId || !this.started) return;
415
+ const res = await this.#request(`${this.serverUrl}/api/bots/${this.botId}/status`);
416
+
417
+ const serverStatus = res?.bot ? res.bot.status : res?.status;
418
+ if (serverStatus && serverStatus !== this.currentStatus) {
419
+ const old = this.currentStatus;
420
+ this.currentStatus = serverStatus;
421
+ console.log(`[mifistix-bot] Статус на сервере изменился: ${old} ➔ ${serverStatus}`);
422
+ if (this.onStatusChange) {
423
+ this.onStatusChange(serverStatus, old);
424
+ }
425
+ }
426
+ } catch (err) {
427
+ // Игнорируем ошибки при опросе
428
+ }
429
+ }
430
+
431
+ #setupGracefulShutdown() {
432
+ const shutdown = async (signal) => {
433
+ console.log(`\n[mifistix-bot] Получен сигнал ${signal}. Завершение работы бота...`);
434
+ await this.stop();
435
+ process.exit(0);
436
+ };
437
+
438
+ this.sigintHandler = () => shutdown('SIGINT');
439
+ this.sigtermHandler = () => shutdown('SIGTERM');
440
+
441
+ process.once('SIGINT', this.sigintHandler);
442
+ process.once('SIGTERM', this.sigtermHandler);
443
+ }
444
+
445
+ async stop() {
446
+ this.polling = false;
447
+
448
+ if (this.heartbeatTimer) {
449
+ clearInterval(this.heartbeatTimer);
450
+ this.heartbeatTimer = null;
451
+ }
452
+ if (this.pollTimer) {
453
+ clearInterval(this.pollTimer);
454
+ this.pollTimer = null;
455
+ }
456
+
457
+ if (this.sigintHandler) {
458
+ process.removeListener('SIGINT', this.sigintHandler);
459
+ }
460
+ if (this.sigtermHandler) {
461
+ process.removeListener('SIGTERM', this.sigtermHandler);
462
+ }
463
+
464
+ if (this.botId && this.currentStatus === 'running') {
465
+ try {
466
+ await this.setStatus('stopped');
467
+ console.log('[mifistix-bot] Бот остановлен');
468
+ } catch {
469
+ /* noop */
470
+ }
471
+ }
472
+
473
+ this.started = false;
474
+ }
475
+ }
476
+
477
+ function start(options) {
478
+ const client = new BotClient(options);
479
+ client.start().catch((err) => {
480
+ console.error('[mifistix-bot] Не удалось запустить бота:', err.message);
481
+ process.exit(1);
482
+ });
483
+ return client;
484
+ }
485
+
486
+ module.exports = { BotClient, start };
package/src/index.js ADDED
@@ -0,0 +1,6 @@
1
+ const { BotClient, start } = require('./client');
2
+
3
+ module.exports = {
4
+ BotClient,
5
+ start,
6
+ };