159 lines
7.4 KiB
JavaScript
159 lines
7.4 KiB
JavaScript
// Function node для n8n — нормализация входящего Webhook MAX
|
||
// Выход: max_id, max_chat_id, answer_text, answer_type, channel: "max", reply_to_*, raw_update
|
||
|
||
const input = $input.first().json;
|
||
// Тело Webhook: в n8n обычно item = { body, headers, params, query }; если прилетел только body — используем input как payload
|
||
let raw = input?.body ?? input;
|
||
if (typeof raw === 'string') {
|
||
try { raw = JSON.parse(raw); } catch (_) {}
|
||
}
|
||
|
||
// ----------------- 0) Игнорируем НЕ-private чаты (группы, каналы) -----------------
|
||
// В MAX в личке приходит recipient с chat_type: "dialog". В группах/каналах — другой chat_type.
|
||
const recipient = raw.message?.recipient ?? raw.recipient;
|
||
const chatType = recipient?.chat_type ?? '';
|
||
if (recipient && chatType !== '' && chatType !== 'dialog') {
|
||
return []; // групповой чат или канал
|
||
}
|
||
|
||
// ----------------- Утилиты -----------------
|
||
const trim = (s) => (s || '').trim();
|
||
const takeLast = (arr) => (Array.isArray(arr) && arr.length ? arr[arr.length - 1] : null);
|
||
const safe = (v, fallback = null) => (v === undefined ? fallback : v);
|
||
|
||
const EMOJI_RE = /[\p{Extended_Pictographic}\u200D\uFE0F]/gu;
|
||
function cleanTextForMeaning(txt) {
|
||
if (!txt) return '';
|
||
const noEmoji = txt.replace(EMOJI_RE, '').replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-./:;<=>?@[\\\]^_`{|}~]/g, ' ');
|
||
return noEmoji.replace(/\s+/g, ' ').trim();
|
||
}
|
||
function isReactionOnly(originalText) {
|
||
if (!originalText) return false;
|
||
const cleaned = cleanTextForMeaning(originalText);
|
||
if (cleaned.length === 0) return true;
|
||
const lettersCount = (cleaned.match(/[A-Za-zА-Яа-я0-9]/g) || []).length;
|
||
return lettersCount < 3 && originalText.trim().length <= 3;
|
||
}
|
||
|
||
// ----------------- Результат -----------------
|
||
const result = {
|
||
max_id: null,
|
||
max_chat_id: null,
|
||
answer_text: null,
|
||
answer_type: null,
|
||
channel: 'max',
|
||
raw_update: raw,
|
||
reply_to_message_id: null,
|
||
reply_to_from_id: null,
|
||
reply_to_from_username: null,
|
||
reply_to_text: null,
|
||
};
|
||
|
||
const msg = raw?.message ?? raw;
|
||
const body = msg?.body;
|
||
const sender = msg?.sender ?? raw?.sender;
|
||
|
||
// ----- 1) ID пользователя и чата (MAX) -----
|
||
// При message_callback сообщение от бота (sender = бот), нажал пользователь — он в raw.callback.user
|
||
const callbackUser = raw?.callback?.user;
|
||
const userId = callbackUser?.user_id ?? callbackUser?.id ?? sender?.user_id ?? sender?.id ?? raw?.user_id ?? msg?.sender?.user_id;
|
||
result.max_id = userId;
|
||
|
||
const chatId =
|
||
msg?.recipient?.chat_id ??
|
||
msg?.recipient?.user_id ??
|
||
msg?.chat_id ??
|
||
recipient?.chat_id ??
|
||
recipient?.user_id ??
|
||
raw?.chat_id ??
|
||
userId;
|
||
result.max_chat_id = chatId;
|
||
|
||
// ----- 2) Ответ на сообщение / пересланное (message.link = LinkedMessage) -----
|
||
const link = msg?.link;
|
||
if (link) {
|
||
result.reply_to_message_id = safe(link.message_id ?? link.id);
|
||
result.reply_to_from_id = safe(link.sender?.user_id ?? link.sender?.id);
|
||
result.reply_to_from_username = safe(link.sender?.username);
|
||
if (link.body?.text) {
|
||
result.reply_to_text = String(link.body.text).replace(/\r?\n/g, ' ').slice(0, 1000);
|
||
} else if (link.body?.attachments?.length) {
|
||
const first = link.body.attachments[0];
|
||
const typeMap = { image: '[photo]', video: '[video]', audio: '[voice]', file: '[document]' };
|
||
result.reply_to_text = typeMap[first?.type] ?? '[attachment]';
|
||
}
|
||
}
|
||
|
||
// ----- 3) Типы входящих: callback, message_created (текст/медиа), bot_started -----
|
||
const updateType = raw.update_type;
|
||
|
||
if (updateType === 'message_callback') {
|
||
// Нажатие кнопки: callback_id для POST /answers, payload — данные кнопки
|
||
const callbackId = raw.callback_id ?? raw.callback?.callback_id ?? msg?.callback_id;
|
||
const payload = raw.callback?.payload ?? raw.callback?.data ?? msg?.callback?.payload ?? msg?.callback?.data;
|
||
result.answer_text = typeof payload === 'string' ? payload : (payload != null ? JSON.stringify(payload) : '');
|
||
result.answer_type = 'callback';
|
||
result.callback_id = callbackId;
|
||
// Текст сообщения с кнопками — чтобы обновить его через POST /answers без кнопок (удалить клавиатуру)
|
||
result.callback_message_text = msg?.body?.text ?? raw.message?.body?.text ?? null;
|
||
result.callback_message_mid = msg?.body?.mid ?? raw.message?.body?.mid ?? null;
|
||
} else if (updateType === 'bot_started') {
|
||
result.answer_text = '/start';
|
||
result.answer_type = 'command';
|
||
} else if (updateType === 'message_created' && body) {
|
||
const hasText = body.text && trim(body.text).length > 0;
|
||
const attachments = body.attachments ?? [];
|
||
const firstAtt = attachments[0];
|
||
|
||
if (firstAtt) {
|
||
const type = firstAtt.type;
|
||
if (type === 'contact') {
|
||
// Поделился контактом (кнопка request_contact). MAX присылает payload.vcf_info (VCARD) и payload.max_info (user).
|
||
const payload = firstAtt.payload ?? {};
|
||
let phone = payload.phone_number ?? payload.phone ?? '';
|
||
if (!phone && payload.vcf_info) {
|
||
const m = payload.vcf_info.match(/TEL[^:]*:([+\d\s\-()]+)/);
|
||
if (m) phone = m[1].replace(/\s/g, '').trim();
|
||
}
|
||
result.answer_text = phone || '[contact]';
|
||
result.answer_type = 'contact';
|
||
result.contact_payload = payload;
|
||
if (payload.max_info) result.contact_name = payload.max_info.name ?? [payload.max_info.first_name, payload.max_info.last_name].filter(Boolean).join(' ');
|
||
} else {
|
||
// Вложение: image | video | audio | file
|
||
result.answer_text = hasText ? body.text.replace(/\r?\n/g, ' ').trim() : (type === 'image' ? '[photo]' : type === 'video' ? '[video]' : type === 'audio' ? '[voice]' : '[document]');
|
||
result.answer_type = type === 'image' ? 'photo' : type === 'video' ? 'video' : type === 'audio' ? 'voice' : 'file';
|
||
if (firstAtt.payload?.token) result.attachment_token = firstAtt.payload.token;
|
||
if (firstAtt.payload?.file_id) result.file_id = firstAtt.payload.file_id;
|
||
if (firstAtt.payload) result.attachment_payload = firstAtt.payload;
|
||
}
|
||
} else if (body.contact) {
|
||
// Контакт в body.contact (альтернативный формат MAX)
|
||
const phone = body.contact.phone_number ?? body.contact.phone ?? '';
|
||
result.answer_text = phone || '[contact]';
|
||
result.answer_type = 'contact';
|
||
result.contact_payload = body.contact;
|
||
} else if (hasText) {
|
||
// Только текст
|
||
const rawText = body.text;
|
||
if (isReactionOnly(rawText)) return [];
|
||
result.answer_text = rawText.replace(/\r?\n/g, ' ').trim();
|
||
result.answer_type = result.answer_text.startsWith('/') ? 'command' : 'text';
|
||
} else {
|
||
return [];
|
||
}
|
||
} else {
|
||
return [];
|
||
}
|
||
|
||
// ----- 4) Валидация -----
|
||
if (result.max_id == null) throw new Error('Не удалось извлечь max_id');
|
||
if (result.max_chat_id == null) throw new Error('Не удалось извлечь max_chat_id');
|
||
if (!result.answer_type) throw new Error('Не удалось определить тип ответа');
|
||
|
||
// ----- 5) Нормализация строк "null" (как в старой ноде) -----
|
||
if (raw.body?.last_name === 'null') raw.body.last_name = null;
|
||
if (result.reply_to_text === 'null') result.reply_to_text = null;
|
||
|
||
return [{ json: result }];
|