// 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 }];