max2tg 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 +78 -0
- package/index.js +547 -0
- package/package.json +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# max2tg
|
|
2
|
+
|
|
3
|
+
Конвертер событий (updates) мессенджера **MAX** в события **Telegram Bot API**.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Установка
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install max2tg
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Быстрый старт
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const { convertMaxToTelegram, convertBatch } = require('max2tg');
|
|
19
|
+
|
|
20
|
+
// Одно событие → массив Telegram updates
|
|
21
|
+
const updates = convertMaxToTelegram(maxUpdate);
|
|
22
|
+
|
|
23
|
+
// Массив событий → плоский массив Telegram updates
|
|
24
|
+
const updates = convertBatch(maxUpdates);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Пример в webhook-обработчике
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
const express = require('express');
|
|
31
|
+
const { convertMaxToTelegram } = require('max2tg');
|
|
32
|
+
|
|
33
|
+
const app = express();
|
|
34
|
+
app.use(express.json());
|
|
35
|
+
|
|
36
|
+
// Webhook от MAX
|
|
37
|
+
app.post('/webhook/max', (req, res) => {
|
|
38
|
+
const tgUpdates = convertMaxToTelegram(req.body);
|
|
39
|
+
|
|
40
|
+
for (const update of tgUpdates) {
|
|
41
|
+
handleTelegramUpdate(update); // ваш общий обработчик
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
res.sendStatus(200);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Webhook от Telegram
|
|
48
|
+
app.post('/webhook/telegram', (req, res) => {
|
|
49
|
+
handleTelegramUpdate(req.body);
|
|
50
|
+
res.sendStatus(200);
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Поддерживаемые типы событий
|
|
57
|
+
|
|
58
|
+
### `bot_started` → `message` с командой `/start`
|
|
59
|
+
Активация бота пользователем, в том числе с `start payload`.
|
|
60
|
+
|
|
61
|
+
### `message_created` → `message`
|
|
62
|
+
Поддержка всех видов вложений, включая геопозицию, документы, стикеры и альбомы.
|
|
63
|
+
Ответы на сообщение, пересланные сообщения, сообщения с клавиатурой и форматированием.
|
|
64
|
+
|
|
65
|
+
### `message_callback` → `callback_query`
|
|
66
|
+
Нажатие пользователя на инлайн-кнопку.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Ограничения
|
|
71
|
+
|
|
72
|
+
Поле file_id всегда равен null, на замену приходит _max_url и _max_token.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Лицензия
|
|
77
|
+
|
|
78
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Конвертер событий (updates) из формата MAX в формат Telegram Bot API.
|
|
3
|
+
*
|
|
4
|
+
* Поддерживаемые типы событий MAX:
|
|
5
|
+
* - bot_started → /start (message с командой)
|
|
6
|
+
* - message_created → message (text / sticker / document / location /
|
|
7
|
+
* forwarded photo / forwarded album)
|
|
8
|
+
* - message_callback → callback_query
|
|
9
|
+
** /
|
|
10
|
+
|
|
11
|
+
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/** Переводит миллисекундный Unix-timestamp в секундный (Telegram). */
|
|
14
|
+
const toSec = (ms) => Math.floor(ms / 1000);
|
|
15
|
+
|
|
16
|
+
/** update_id генерируем из timestamp (секунды). Принимает ms или sec. */
|
|
17
|
+
const toUpdateId = (ts) => ts > 1e12 ? Math.floor(ts / 1000) : ts;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Собирает минимальный объект Chat из данных MAX.
|
|
21
|
+
* MAX хранит chat_id и user_id в recipient; для личных диалогов
|
|
22
|
+
* Telegram использует тот же id что и у пользователя.
|
|
23
|
+
*/
|
|
24
|
+
function buildChat(maxUser) {
|
|
25
|
+
return {
|
|
26
|
+
id: maxUser.user_id, // в диалоге chat.id = user.id
|
|
27
|
+
type: "private",
|
|
28
|
+
first_name: maxUser.first_name,
|
|
29
|
+
last_name: maxUser.last_name ?? undefined,
|
|
30
|
+
username: maxUser.username ?? undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Собирает объект From/User из данных MAX.
|
|
36
|
+
*/
|
|
37
|
+
function buildFrom(maxUser) {
|
|
38
|
+
return {
|
|
39
|
+
id: maxUser.user_id,
|
|
40
|
+
is_bot: maxUser.is_bot ?? false,
|
|
41
|
+
first_name: maxUser.first_name,
|
|
42
|
+
last_name: maxUser.last_name ?? undefined,
|
|
43
|
+
username: maxUser.username ?? undefined,
|
|
44
|
+
language_code: undefined, // MAX не передаёт language_code
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── markup (MAX) → entities (Telegram) ─────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
const MAX_MARKUP_TO_TG_ENTITY = {
|
|
51
|
+
strong: "bold",
|
|
52
|
+
em: "italic",
|
|
53
|
+
pre: "pre",
|
|
54
|
+
code: "code",
|
|
55
|
+
link: "url",
|
|
56
|
+
strikethrough: "strikethrough",
|
|
57
|
+
underline: "underline",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Конвертирует MAX markup-массив в массив Telegram MessageEntity.
|
|
62
|
+
* Дубликаты (MAX иногда присылает одну и ту же метку дважды) убираются.
|
|
63
|
+
*/
|
|
64
|
+
function convertMarkup(markup = []) {
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
const entities = [];
|
|
67
|
+
|
|
68
|
+
for (const m of markup) {
|
|
69
|
+
const type = MAX_MARKUP_TO_TG_ENTITY[m.type];
|
|
70
|
+
if (!type) continue;
|
|
71
|
+
|
|
72
|
+
const key = `${type}:${m.from}:${m.length}`;
|
|
73
|
+
if (seen.has(key)) continue;
|
|
74
|
+
seen.add(key);
|
|
75
|
+
|
|
76
|
+
entities.push({ type, offset: m.from, length: m.length });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return entities.length ? entities : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── inline_keyboard (MAX) → reply_markup (Telegram) ────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* MAX button → Telegram InlineKeyboardButton
|
|
86
|
+
*
|
|
87
|
+
* Типы MAX: link | callback | (возможно request_geo, request_contact и др.)
|
|
88
|
+
*/
|
|
89
|
+
function convertButton(maxBtn) {
|
|
90
|
+
const tgBtn = { text: maxBtn.text };
|
|
91
|
+
|
|
92
|
+
if (maxBtn.type === "link" || maxBtn.url) {
|
|
93
|
+
tgBtn.url = maxBtn.url;
|
|
94
|
+
} else if (maxBtn.type === "callback") {
|
|
95
|
+
tgBtn.callback_data = maxBtn.payload ?? "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return tgBtn;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function convertInlineKeyboard(attachment) {
|
|
102
|
+
if (!attachment || attachment.type !== "inline_keyboard") return undefined;
|
|
103
|
+
const rows = attachment.payload?.buttons ?? [];
|
|
104
|
+
return {
|
|
105
|
+
inline_keyboard: rows.map((row) => row.map(convertButton)),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── attachments ─────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Извлекает первый inline_keyboard из массива вложений MAX.
|
|
113
|
+
*/
|
|
114
|
+
function extractKeyboard(attachments = []) {
|
|
115
|
+
return (
|
|
116
|
+
attachments.find((a) => a.type === "inline_keyboard") ?? null
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Строит объект Telegram стикера из MAX-вложения типа sticker.
|
|
122
|
+
* В MAX нет file_id — передаём только то, что есть.
|
|
123
|
+
*/
|
|
124
|
+
function buildSticker(att) {
|
|
125
|
+
return {
|
|
126
|
+
// file_id недоступен из MAX — ставим заглушку; реальный file_id
|
|
127
|
+
// получить нельзя без загрузки файла через Telegram API.
|
|
128
|
+
file_id: null,
|
|
129
|
+
file_unique_id: null,
|
|
130
|
+
width: att.width ?? 512,
|
|
131
|
+
height: att.height ?? 512,
|
|
132
|
+
is_animated: false,
|
|
133
|
+
is_video: false,
|
|
134
|
+
type: "regular",
|
|
135
|
+
thumbnail: att.payload?.url
|
|
136
|
+
? {
|
|
137
|
+
file_id: null,
|
|
138
|
+
file_unique_id: null,
|
|
139
|
+
width: att.width ?? 128,
|
|
140
|
+
height: att.height ?? 128,
|
|
141
|
+
}
|
|
142
|
+
: undefined,
|
|
143
|
+
// Нативный URL стикера (MAX-специфично, не часть TG API)
|
|
144
|
+
_max_url: att.payload?.url,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Строит объект Telegram Document из MAX-вложения типа file.
|
|
150
|
+
*/
|
|
151
|
+
function buildDocument(att) {
|
|
152
|
+
return {
|
|
153
|
+
file_id: null, // недоступен без Telegram
|
|
154
|
+
file_unique_id: null,
|
|
155
|
+
file_name: att.filename ?? undefined,
|
|
156
|
+
file_size: att.size ?? undefined,
|
|
157
|
+
// Прямая ссылка из MAX (временная, требует токена)
|
|
158
|
+
_max_url: att.payload?.url,
|
|
159
|
+
_max_token: att.payload?.token,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Строит массив PhotoSize (один размер) из MAX-вложения типа image.
|
|
165
|
+
*/
|
|
166
|
+
function buildPhoto(att) {
|
|
167
|
+
return [
|
|
168
|
+
{
|
|
169
|
+
file_id: null,
|
|
170
|
+
file_unique_id: null,
|
|
171
|
+
width: 0,
|
|
172
|
+
height: 0,
|
|
173
|
+
_max_url: att.payload?.url,
|
|
174
|
+
_max_token: att.payload?.token,
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Строит Telegram Location из MAX-вложения типа location.
|
|
181
|
+
*/
|
|
182
|
+
function buildLocation(att) {
|
|
183
|
+
return {
|
|
184
|
+
latitude: att.latitude,
|
|
185
|
+
longitude: att.longitude,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── forward origin ──────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Строит forward_origin из MAX link.type === 'forward'.
|
|
193
|
+
* MAX хранит sender пересланного сообщения; если это бот/канал — channel,
|
|
194
|
+
* иначе — user.
|
|
195
|
+
*/
|
|
196
|
+
function buildForwardOrigin(maxLink, dateMs) {
|
|
197
|
+
if (!maxLink || maxLink.type !== "forward") return {};
|
|
198
|
+
|
|
199
|
+
const date = toSec(dateMs);
|
|
200
|
+
const sender = maxLink.sender ?? {};
|
|
201
|
+
|
|
202
|
+
if (sender.is_bot || !sender.user_id) {
|
|
203
|
+
// Предполагаем канал / публичный источник
|
|
204
|
+
return {
|
|
205
|
+
forward_origin: {
|
|
206
|
+
type: "channel",
|
|
207
|
+
date,
|
|
208
|
+
chat: {
|
|
209
|
+
id: maxLink.chat_id || 0,
|
|
210
|
+
type: "channel",
|
|
211
|
+
title: sender.first_name ?? "Unknown",
|
|
212
|
+
username: sender.username ?? undefined,
|
|
213
|
+
},
|
|
214
|
+
message_id: 0,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
forward_origin: {
|
|
221
|
+
type: "user",
|
|
222
|
+
date,
|
|
223
|
+
sender_user: buildFrom(sender),
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ─── reply_to_message ────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Строит упрощённый reply_to_message из MAX link.type === 'reply'.
|
|
232
|
+
*/
|
|
233
|
+
function buildReplyTo(maxLink, chat) {
|
|
234
|
+
if (!maxLink || maxLink.type !== "reply") return {};
|
|
235
|
+
|
|
236
|
+
const sender = maxLink.sender ?? {};
|
|
237
|
+
return {
|
|
238
|
+
reply_to_message: {
|
|
239
|
+
message_id: maxLink.message?.mid ?? null,
|
|
240
|
+
chat,
|
|
241
|
+
from: buildFrom(sender),
|
|
242
|
+
date: 0,
|
|
243
|
+
text: maxLink.message?.text ?? "",
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ─── конвертеры по update_type ───────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* bot_started → Telegram update с /start
|
|
252
|
+
*/
|
|
253
|
+
function convertBotStarted(maxUpdate) {
|
|
254
|
+
const user = maxUpdate.user;
|
|
255
|
+
const chat = buildChat(user);
|
|
256
|
+
const from = buildFrom(user);
|
|
257
|
+
const date = toSec(maxUpdate.timestamp);
|
|
258
|
+
|
|
259
|
+
const startText = maxUpdate.payload
|
|
260
|
+
? `/start ${maxUpdate.payload}`
|
|
261
|
+
: "/start";
|
|
262
|
+
|
|
263
|
+
const entities = [{ type: "bot_command", offset: 0, length: 6 }];
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
update_id: toUpdateId(maxUpdate.timestamp),
|
|
267
|
+
message: {
|
|
268
|
+
message_id: String(toSec(maxUpdate.timestamp)), // bot_started не имеет mid — используем timestamp
|
|
269
|
+
from,
|
|
270
|
+
chat,
|
|
271
|
+
date,
|
|
272
|
+
text: startText,
|
|
273
|
+
entities,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* message_created → Telegram update (message / стикер / документ /
|
|
280
|
+
* геопозиция / forward / album-item)
|
|
281
|
+
*
|
|
282
|
+
* Возвращает МАССИВ updates — один или несколько (для альбомов).
|
|
283
|
+
*/
|
|
284
|
+
function convertMessageCreated(maxUpdate) {
|
|
285
|
+
const msg = maxUpdate.message;
|
|
286
|
+
const body = msg.body;
|
|
287
|
+
const sender = msg.sender;
|
|
288
|
+
const ts = toSec(msg.timestamp);
|
|
289
|
+
const chat = buildChat(sender);
|
|
290
|
+
const from = buildFrom(sender);
|
|
291
|
+
const link = msg.link ?? null;
|
|
292
|
+
|
|
293
|
+
// Базовая обёртка Telegram message
|
|
294
|
+
const mid = body.mid ?? null;
|
|
295
|
+
const base = {
|
|
296
|
+
message_id: mid, // MAX mid передаётся как строка
|
|
297
|
+
from,
|
|
298
|
+
chat,
|
|
299
|
+
date: ts,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// Forward/reply мета
|
|
303
|
+
const forwardMeta = buildForwardOrigin(link, msg.timestamp);
|
|
304
|
+
const replyMeta = buildReplyTo(link, chat);
|
|
305
|
+
|
|
306
|
+
const attachments = body.attachments ?? [];
|
|
307
|
+
|
|
308
|
+
// ── Стикер ──────────────────────────────────────────────────────────────
|
|
309
|
+
const stickerAtt = attachments.find((a) => a.type === "sticker");
|
|
310
|
+
if (stickerAtt) {
|
|
311
|
+
return [
|
|
312
|
+
{
|
|
313
|
+
update_id: toUpdateId(msg.timestamp),
|
|
314
|
+
message: { ...base, sticker: buildSticker(stickerAtt) },
|
|
315
|
+
},
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── Геопозиция ───────────────────────────────────────────────────────────
|
|
320
|
+
const locationAtt = attachments.find((a) => a.type === "location");
|
|
321
|
+
if (locationAtt) {
|
|
322
|
+
return [
|
|
323
|
+
{
|
|
324
|
+
update_id: toUpdateId(msg.timestamp),
|
|
325
|
+
message: { ...base, location: buildLocation(locationAtt) },
|
|
326
|
+
},
|
|
327
|
+
];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── Файл / документ ──────────────────────────────────────────────────────
|
|
331
|
+
const fileAtt = attachments.find((a) => a.type === "file");
|
|
332
|
+
if (fileAtt) {
|
|
333
|
+
return [
|
|
334
|
+
{
|
|
335
|
+
update_id: toUpdateId(msg.timestamp),
|
|
336
|
+
message: {
|
|
337
|
+
...base,
|
|
338
|
+
document: buildDocument(fileAtt),
|
|
339
|
+
caption: body.text || undefined,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── Видео ─────────────────────────────────────────────────────────────────
|
|
346
|
+
const videoAtt = attachments.find((a) => a.type === "video");
|
|
347
|
+
|
|
348
|
+
// ── Изображения ──────────────────────────────────────────────────────────
|
|
349
|
+
const imageAtts = attachments.filter((a) => a.type === "image");
|
|
350
|
+
|
|
351
|
+
// ── Альбом (видео + изображения или несколько изображений) ───────────────
|
|
352
|
+
// MAX присылает всё одним событием; Telegram шлёт каждый элемент отдельно
|
|
353
|
+
// с общим media_group_id. Генерируем несколько updates.
|
|
354
|
+
const mediaItems = [
|
|
355
|
+
...(videoAtt ? [{ type: "video", att: videoAtt }] : []),
|
|
356
|
+
...imageAtts.map((att) => ({ type: "image", att })),
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
if (mediaItems.length > 1) {
|
|
360
|
+
const mediaGroupId = String(Date.now()); // псевдо media_group_id
|
|
361
|
+
const caption = body.text || undefined;
|
|
362
|
+
const captionEntities = convertMarkup(body.markup);
|
|
363
|
+
|
|
364
|
+
return mediaItems.map((item, idx) => {
|
|
365
|
+
const isFirst = idx === 0;
|
|
366
|
+
const tgMsg = {
|
|
367
|
+
...base,
|
|
368
|
+
...forwardMeta,
|
|
369
|
+
media_group_id: mediaGroupId,
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
if (item.type === "video") {
|
|
373
|
+
tgMsg.video = {
|
|
374
|
+
file_id: null,
|
|
375
|
+
file_unique_id: null,
|
|
376
|
+
width: 0,
|
|
377
|
+
height: 0,
|
|
378
|
+
duration: item.att.duration ?? 0,
|
|
379
|
+
_max_url: item.att.payload?.url,
|
|
380
|
+
};
|
|
381
|
+
} else {
|
|
382
|
+
tgMsg.photo = buildPhoto(item.att);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Caption только к первому элементу (как в Telegram)
|
|
386
|
+
if (isFirst && caption) {
|
|
387
|
+
tgMsg.caption = caption;
|
|
388
|
+
if (captionEntities) tgMsg.caption_entities = captionEntities;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { update_id: toUpdateId(msg.timestamp) + idx, message: tgMsg };
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ── Одиночное изображение ─────────────────────────────────────────────────
|
|
396
|
+
if (mediaItems.length === 1) {
|
|
397
|
+
const item = mediaItems[0];
|
|
398
|
+
const tgMsg = { ...base, ...forwardMeta, ...replyMeta };
|
|
399
|
+
|
|
400
|
+
if (item.type === "video") {
|
|
401
|
+
tgMsg.video = {
|
|
402
|
+
file_id: null,
|
|
403
|
+
file_unique_id: null,
|
|
404
|
+
width: 0,
|
|
405
|
+
height: 0,
|
|
406
|
+
duration: item.att.duration ?? 0,
|
|
407
|
+
_max_url: item.att.payload?.url,
|
|
408
|
+
};
|
|
409
|
+
} else {
|
|
410
|
+
tgMsg.photo = buildPhoto(item.att);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const caption = body.text || undefined;
|
|
414
|
+
if (caption) {
|
|
415
|
+
tgMsg.caption = caption;
|
|
416
|
+
const ents = convertMarkup(body.markup);
|
|
417
|
+
if (ents) tgMsg.caption_entities = ents;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return [{ update_id: toUpdateId(msg.timestamp), message: tgMsg }];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── Inline keyboard в теле сообщения (бот → пользователь) ────────────────
|
|
424
|
+
const kbAtt = extractKeyboard(attachments);
|
|
425
|
+
const replyMarkup = convertInlineKeyboard(kbAtt);
|
|
426
|
+
|
|
427
|
+
// ── Обычный текст ─────────────────────────────────────────────────────────
|
|
428
|
+
const text = body.text ?? "";
|
|
429
|
+
const entities = convertMarkup(body.markup);
|
|
430
|
+
|
|
431
|
+
const tgMsg = {
|
|
432
|
+
...base,
|
|
433
|
+
...forwardMeta,
|
|
434
|
+
...replyMeta,
|
|
435
|
+
text,
|
|
436
|
+
};
|
|
437
|
+
if (entities) tgMsg.entities = entities;
|
|
438
|
+
if (replyMarkup) tgMsg.reply_markup = replyMarkup;
|
|
439
|
+
|
|
440
|
+
return [{ update_id: toUpdateId(msg.timestamp), message: tgMsg }];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* message_callback → Telegram callback_query
|
|
445
|
+
*/
|
|
446
|
+
function convertMessageCallback(maxUpdate) {
|
|
447
|
+
const cb = maxUpdate.callback;
|
|
448
|
+
const user = cb.user;
|
|
449
|
+
const from = buildFrom(user);
|
|
450
|
+
const chat = buildChat(user);
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
update_id: toUpdateId(cb.timestamp),
|
|
454
|
+
callback_query: {
|
|
455
|
+
id: String(cb.timestamp),
|
|
456
|
+
from,
|
|
457
|
+
message: {
|
|
458
|
+
message_id: maxUpdate.message?.body?.mid ?? String(toSec(cb.timestamp)),
|
|
459
|
+
from: {
|
|
460
|
+
id: 0,
|
|
461
|
+
is_bot: true,
|
|
462
|
+
first_name: "Bot",
|
|
463
|
+
},
|
|
464
|
+
chat,
|
|
465
|
+
date: toSec(cb.timestamp),
|
|
466
|
+
text: maxUpdate.message?.body?.text ?? "",
|
|
467
|
+
reply_markup: (() => {
|
|
468
|
+
const kbAtt = extractKeyboard(maxUpdate.message?.body?.attachments ?? []);
|
|
469
|
+
return convertInlineKeyboard(kbAtt);
|
|
470
|
+
})(),
|
|
471
|
+
},
|
|
472
|
+
chat_instance: String(chat.id),
|
|
473
|
+
data: cb.payload ?? "",
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ─── главная функция ─────────────────────────────────────────────────────────
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Конвертирует одно MAX-событие в один или несколько Telegram-updates.
|
|
482
|
+
*
|
|
483
|
+
* @param {object} maxUpdate — тело события из MAX (поле `max` в тестовом JSON)
|
|
484
|
+
* @returns {object[]} — массив Telegram update-объектов
|
|
485
|
+
*/
|
|
486
|
+
function convertMaxToTelegram(maxUpdate) {
|
|
487
|
+
const type = maxUpdate.update_type;
|
|
488
|
+
|
|
489
|
+
switch (type) {
|
|
490
|
+
case "bot_started":
|
|
491
|
+
return [convertBotStarted(maxUpdate)];
|
|
492
|
+
|
|
493
|
+
case "message_created":
|
|
494
|
+
return convertMessageCreated(maxUpdate);
|
|
495
|
+
|
|
496
|
+
case "message_callback":
|
|
497
|
+
return [convertMessageCallback(maxUpdate)];
|
|
498
|
+
|
|
499
|
+
default:
|
|
500
|
+
console.warn(`[max-to-telegram] Неизвестный update_type: "${type}"`);
|
|
501
|
+
return [];
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Удобная обёртка: принимает массив MAX-событий, возвращает плоский
|
|
507
|
+
* массив Telegram updates.
|
|
508
|
+
*
|
|
509
|
+
* @param {object[]} maxUpdates
|
|
510
|
+
* @returns {object[]}
|
|
511
|
+
*/
|
|
512
|
+
function convertBatch(maxUpdates) {
|
|
513
|
+
return maxUpdates.flatMap(convertMaxToTelegram);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
module.exports = { convertMaxToTelegram, convertBatch };
|
|
517
|
+
|
|
518
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
519
|
+
// NOTES — Нюансы и ограничения
|
|
520
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
521
|
+
//
|
|
522
|
+
// 1. file_id / file_unique_id поля выставляются в null.
|
|
523
|
+
// Добавляются (_max_url + _max_token)
|
|
524
|
+
//
|
|
525
|
+
// 2. message_id
|
|
526
|
+
// В MAX строка вида "mid.xxxxx" передаётся в поле message_id.
|
|
527
|
+
//
|
|
528
|
+
// 3. Альбомы (media group)
|
|
529
|
+
// MAX присылает весь альбом одним событием message_created с несколькими
|
|
530
|
+
// вложениями. Telegram рассылает каждый элемент отдельным update с единым
|
|
531
|
+
// media_group_id. convertMessageCreated возвращает массив — разошлите каждый
|
|
532
|
+
// элемент отдельно или обработайте весь массив как альбом.
|
|
533
|
+
//
|
|
534
|
+
// 4. callback_data
|
|
535
|
+
// MAX payload передаётся в callback_data без изменений. Если payload
|
|
536
|
+
// длиннее 64 байт — Telegram Bot API вернёт ошибку при отправке.
|
|
537
|
+
// Рекомендуется хранить длинные payload на стороне бэкенда и передавать
|
|
538
|
+
// лишь короткий ключ уже на уровне формирования кнопок в MAX.
|
|
539
|
+
//
|
|
540
|
+
// 5. Forward origin из приватных источников
|
|
541
|
+
// Если оригинал переслан из закрытого чата, MAX может не передать
|
|
542
|
+
// полные данные отправителя. forward_origin.chat.id будет 0.
|
|
543
|
+
//
|
|
544
|
+
// 6. update_id
|
|
545
|
+
// Генерируется из Unix-timestamp события (в секундах). При нескольких
|
|
546
|
+
// событиях в одну секунду возможны коллизии — для альбомов используется
|
|
547
|
+
// timestamp + порядковый индекс элемента.
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "max2tg",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MAX to Telegram event style converter",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"max",
|
|
8
|
+
"telegram",
|
|
9
|
+
"bot",
|
|
10
|
+
"converter",
|
|
11
|
+
"adapter"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/azizbots/max2tg.git"
|
|
17
|
+
}
|
|
18
|
+
}
|