{
"updatedAt": "2025-12-10T02:57:21.000Z",
"createdAt": "2025-12-09T17:22:27.435Z",
"id": "HfvL0uSdAtwjBN7u",
"name": "Geni_AI_v015-3",
"description": null,
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "78542b6d-15c2-4809-935c-49d1e17dcc1e",
"leftValue": "={{ $json.tool }}",
"rightValue": "account",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "dfbae9da-3cc7-488e-9f71-f21a4b9b4345",
"leftValue": "={{ $json.tool }}",
"rightValue": "lang",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "19714412-73ff-4c46-ba9e-4f4276157e72",
"leftValue": "={{ $json.tool }}",
"rightValue": "instruction",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.tool }}",
"rightValue": "=flux",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
1328,
2016
],
"id": "f6103241-cbad-4932-a4e4-d845d2eb65c2",
"name": "Switch1"
},
{
"parameters": {
"updates": [
"message",
"callback_query",
"pre_checkout_query"
],
"additionalFields": {}
},
"id": "04d5c88f-05c1-49e8-b34b-26ceb2ab22d2",
"name": "Telegram Trigger1",
"type": "n8n-nodes-base.telegramTrigger",
"typeVersion": 1,
"position": [
-1568,
1952
],
"webhookId": "aaa5322f-da8f-4d75-b626-ae10bb379fbe",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"method": "POST",
"url": "https://api.telegram.org/bot8495842221:AAHBriXvzudyazB2f8z3ilGQ8A5ESdjjtl8/sendMessage",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "chat_id",
"value": "={{ $json.chatId }}"
},
{
"name": "text",
"value": "={{ $json.replyText }}"
},
{
"name": "parse_mode",
"value": "HTML"
},
{
"name": "reply_markup",
"value": "={{ $json.replyMarkup }}"
},
{
"name": "disable_notification",
"value": "={{ $json.silent }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
624,
1792
],
"id": "69d935ba-a076-4654-bc52-a14458e6180e",
"name": "reply_markup",
"retryOnFail": true
},
{
"parameters": {
"jsCode": "// === MENU (RU/EN localization) ===\n// Keep original logic; add Account section + \"flat\" actions inside Account\n\n// ---- state\nconst sd = $getWorkflowStaticData('global');\nsd.menu = sd.menu || {};\nsd.session = sd.session || {};\nsd.userLang = sd.userLang || {};\nsd.userSub = sd.userSub || {}; // { [chatId]: { active: boolean } } optional cache\n\n// ---- input\nconst msg = $json.message || {};\nconst chatId = String(msg.chat?.id || $json.chat?.id || '');\nconst msgId = msg.message_id;\nconst textIn = String(msg.text ?? msg.caption ?? '').trim();\n\n// ---- language resolve: 1) payload.lang 2) cached 3) Telegram UI hint 4) ru\nlet lang = (typeof $json.lang === 'string' && /^(ru|en)$/i.test($json.lang)) ? $json.lang.toLowerCase() : null;\nif (!lang) lang = sd.userLang[chatId] || null;\nif (!lang) lang = (/^en/i.test(String(msg.from?.language_code || ''))) ? 'en' : 'ru';\nsd.userLang[chatId] = lang;\n\n// ---- localization dict\nconst I18N = {\n ru: {\n home: '🏠 Главная',\n back: '⬅️ Назад',\n account: '👤 Аккаунт',\n mainTitle: '🏠 Главная страница',\n mainDesc: 'Выберите инструмент',\n help: '❔ Помощь',\n helpDesc: 'Раздел помощи',\n helpInstruction: '📘 Инструкция',\n helpInstructionDesc: 'Как пользоваться ботом',\n language: '🌐 Язык',\n languageDesc: 'Выбор языка интерфейса',\n image: '🖼 Image',\n imageDesc: 'Генерация изображений',\n gpt: '💬 GPT',\n gptDesc: 'Чат с LLM',\n audio: '🎵 Audio',\n audioDesc: 'Аудио-инструменты',\n video: '🎬 Video',\n videoDesc: 'Видео-инструменты',\n seedream: '🌊 Seedream',\n nanobanana: '🍌 Nano Banana',\n flux: '⚡️ FLUX',\n fluxDesc: 'Генерация изображения с FLUX',\n seedance: '🎞 Seedance',\n veo: '⭕ Veo',\n sora: '🌙 Sora 2',\n settings: '⚙️ Settings',\n placeholder: 'Меню',\n\n // Account\n accTitle: '👤 Аккаунт',\n accDesc: 'Управление аккаунтом',\n accBalance: '💰 Баланс',\n accStatus: '📦 Статус',\n accBuySub: '💳 Купить подписку',\n accBuyCreds: '⚡️ Купить кредиты',\n\n // NEW: Community\n community: '👥 Сообщество',\n communityDesc: 'Ссылка на канал @voyakin_geni',\n },\n en: {\n home: '🏠 Home',\n back: '⬅️ Back',\n account: '👤 Account',\n mainTitle: '🏠 Home',\n mainDesc: 'Pick a tool',\n help: '❔ Help',\n helpDesc: 'Help section',\n helpInstruction: '📘 Instructions',\n helpInstructionDesc: 'How to use the bot',\n language: '🌐 Language',\n languageDesc: 'Choose interface language',\n image: '🖼 Image',\n imageDesc: 'Image generation',\n gpt: '💬 GPT',\n gptDesc: 'Chat with LLM',\n audio: '🎵 Audio',\n audioDesc: 'Audio tools',\n video: '🎬 Video',\n videoDesc: 'Video tools',\n seedream: '🌊 Seedream',\n nanobanana: '🍌 Nano Banana',\n flux: '⚡️ FLUX',\n fluxDesc: 'Generating an image with FLUX',\n seedance: '🎞 Seedance',\n veo: '⭕ Veo',\n sora: '🌙 Sora 2',\n settings: '⚙️ Settings',\n placeholder: 'Menu',\n\n // Account\n accTitle: '👤 Account',\n accDesc: 'Manage your account',\n accBalance: '💰 Balance',\n accStatus: '📦 Status',\n accBuySub: '💳 Buy subscription',\n accBuyCreds: '⚡️ Buy credits',\n\n // NEW: Community\n community: '👥 Community',\n communityDesc: 'Link to channel @voyakin_geni',\n }\n};\nconst L = (k)=> (I18N[lang]||I18N.ru)[k] ?? k;\n\n// ---- webapps\nconst WEBAPP_ACCOUNT = 'https://tgmenu.pages.dev/account';\nconst WEBAPP_SETTINGS = 'https://app-ru.geni-ai.online/settings';\n\n// ---- menu tree (localized)\nconst MENU = {\n key: '', title: L('mainTitle'), desc: L('mainDesc'), children: [\n { key: 'image', title: L('image'), desc: L('imageDesc'), children: [\n // [V1 HIDE] Seedream / Nano Banana временно отключены\n { key: 'seedream', title: L('seedream'), desc: '—', command: 'seedream', children: [\n { key: 'settings', title: L('settings'), type: 'webapp', webUrl: WEBAPP_SETTINGS + '/seedream.html?tool=seedream' },\n ]},\n // { key: 'nano banana', title: L('nanobanana'), desc: '—', command: 'nano banana', children: [\n // { key: 'settings', title: L('settings'), type: 'webapp', webUrl: WEBAPP_SETTINGS + '/nanobanana.html' },\n // ]},\n { key: 'flux', title: L('flux'), desc: L('fluxDesc'), command: 'flux', children: [\n { key: 'settings', title: L('settings'), type: 'webapp', webUrl: WEBAPP_SETTINGS + '/flux3.html?tool=flux' },\n ]},\n ]},\n\n // [V1 HIDE] GPT / Audio / Video временно отключены\n // { key: 'gpt', title: L('gpt'), desc: L('gptDesc'), command: 'gpt' },\n // { key: 'audio', title: L('audio'), desc: L('audioDesc'), command: 'audio' },\n // { key: 'video', title: L('video'), desc: L('videoDesc'), command: 'video', children: [\n // { key: 'seedance', title: L('seedance'), desc: '—', command: 'seedance', children: [\n // { key: 'settings', title: L('settings'), type: 'webapp', webUrl: WEBAPP_SETTINGS + '/seedance.html' }\n // ]},\n // { key: 'veo', title: L('veo'), desc: '—', command: 'veo', children: [\n // { key: 'settings', title: L('settings'), type: 'webapp', webUrl: WEBAPP_SETTINGS + '/veo.html' }\n // ]},\n // { key: 'sora', title: L('sora'), desc: '—', command: 'sora', children: [\n // { key: 'settings', title: L('settings'), type: 'webapp', webUrl: WEBAPP_SETTINGS + '/sora_2.html' }\n // ]},\n // ]},\n\n { key: 'help', title: L('help'), desc: L('helpDesc'), children: [\n { key: 'language', title: L('language'), desc: L('languageDesc'), command: 'lang' },\n { key: 'instruction', title: L('helpInstruction'), desc: L('helpInstructionDesc'), command: 'instruction' },\n ]},\n\n // ACCOUNT (top-level)\n { key: 'account', title: L('accTitle'), desc: L('accDesc'), command: 'account', children: [\n { key: 'balance', title: L('accBalance'), desc: '—' },\n // { key: 'status', title: L('accStatus'), desc: '—' },\n { key: 'buy_sub', title: L('accBuySub'), desc: '—' }, // UI-сильно скрыт ниже\n { key: 'buy_credits', title: L('accBuyCreds'), desc: '—' },\n ]},\n\n { key: 'community', title: L('community'), desc: L('communityDesc'), command: 'community' },\n ]\n};\n\n// ---- helpers\nconst BTN_MAIN = L('home');\nconst BTN_BACK = L('back');\nconst BTN_ACCOUNT = L('account'); // web_app button text at bottom (сейчас не используем)\n\nconst hasMedia =\n (Array.isArray(msg.photo) && msg.photo.length>0) ||\n !!msg.document;\n\nconst norm = s => (s||'').normalize('NFKC').toLowerCase()\n .replace(/[\\p{Emoji_Presentation}\\p{Emoji}\\uFE0F]/gu,'')\n .replace(/[\\u0300-\\u036f]/g,'')\n .replace(/[^\\p{L}\\p{N}]+/gu,'').trim();\n\nconst split = p => p ? p.split('/').filter(Boolean) : [];\nfunction nodeByPath(path){\n if(!path) return MENU;\n let n=MENU;\n for(const part of split(path)){\n n=(n.children||[]).find(c=>c.key===part);\n if(!n) return MENU;\n }\n return n;\n}\nfunction parentPath(path){ const a=split(path); a.pop(); return a.join('/'); }\nfunction childKeyByInput(path,txt){\n const want=norm(txt); const n=nodeByPath(path);\n const here=(n.children||[]).find(c=>[c.key,c.title].some(v=>norm(v)===want))?.key;\n if (here) return here;\n const root=(MENU.children||[]).find(c=>[c.key,c.title].some(v=>norm(v)===want))?.key;\n return root||null;\n}\nfunction matchInAccount(txt){\n const acc = (MENU.children||[]).find(c=>c.key==='account');\n if (!acc) return null;\n const want = norm(txt);\n const hit = (acc.children||[]).find(c => [c.key,c.title].some(v => norm(v)===want));\n return hit?.key || null; // 'balance'|'status'|'buy_sub'|'buy_credits'\n}\n\n// ---- nav/mode\nlet path = sd.menu[chatId]?.path || '';\nlet session = sd.session[chatId] || { tool:null };\nlet account_action = null; // <- FLAT actions inside account\n\nconst low = norm(textIn);\n\n// отдельный флаг именно для /start\nconst isStartCmd = /^\\/start\\b/i.test(textIn);\n\n// \"меню\" = /start, /menu и текстовые синонимы\nconst isMenuCmd =\n isStartCmd ||\n /^\\/menu\\b/i.test(textIn) ||\n ['menu','main','home','меню','главная','домой'].includes(low);\n\nconst isBack = [norm(BTN_BACK),'back','назад'].includes(low);\n\n// команды/шорткаты\nconst isBalanceCmd = /^\\/balance\\b/i.test(textIn) || low === 'balance' || low === norm(L('accBalance'));\nconst isBuyCreditsCmd = /^\\/buy_credits\\b/i.test(textIn) || low === 'buycredits' || low === 'buy_credits';\nconst isLangCmd = /^\\/lang\\b/i.test(textIn) || low === 'lang';\n\n// navigation changes\nif (isMenuCmd) {\n path=''; session.tool=null;\n} else if (isBack) {\n const prev=parentPath(path);\n if (split(path).length===1) session.tool=null;\n path=prev;\n} else if (textIn) {\n // direct \"Language\" jump: кнопка «Язык» ИЛИ /lang / lang\n if (norm(textIn) === norm(L('language')) || isLangCmd) {\n path='help/language';\n session.tool='lang';\n } else if (isBalanceCmd) {\n // прямой вызов баланса: /balance или \"Баланс\"\n path = 'account';\n account_action = 'balance';\n session.tool = 'account';\n } else if (isBuyCreditsCmd) {\n // прямой вызов покупки кредитов: /buy_credits\n path = 'account';\n account_action = 'buy_credits';\n session.tool = 'account';\n } else {\n const k = childKeyByInput(path, textIn); // can be child of current or root\n if (k){\n const isRootChild = (MENU.children||[]).some(c=>c.key===k);\n\n if (isRootChild) {\n // go to root child as usual\n path = k;\n const chosen = nodeByPath(path);\n if (chosen?.command) session.tool = chosen.command;\n else session.tool = null;\n } else {\n // child of current node\n if (path === 'account') {\n // FLAT actions: keep path at 'account' and emit action\n account_action = k; // 'balance'|'status'|'buy_sub'|'buy_credits'\n session.tool = 'account';\n // DO NOT change path\n } else {\n // normal deep navigation for other sections\n path = path ? `${path}/${k}` : k;\n const chosen = nodeByPath(path);\n if (chosen?.command) session.tool = chosen.command;\n else if (!chosen?.children?.length) session.tool = null;\n }\n }\n }\n }\n}\n\n// persist\nsd.menu[chatId] = { path, updatedAt: new Date().toISOString(), lang };\nsd.session[chatId] = session;\n\nconst node = nodeByPath(path);\nconst cameFromNav = isMenuCmd || isBack || !!childKeyByInput(parentPath(path), textIn);\nconst isLangTool = session.tool === 'lang';\nconst isAccountAction = session.tool === 'account' && !!account_action;\nconst isInstructionTool = session.tool === 'instruction';\n\n// shouldSendMenu: no menu when lang inline or account action (we'll send specific message)\nconst shouldSendMenu = (isLangTool || isAccountAction || isInstructionTool) ? false : cameFromNav;\nconst isPrompt = !!session.tool && !shouldSendMenu && (textIn || hasMedia);\n\n// ====== Home inline buttons (only on root + когда реально рисуем меню) ======\nlet homeInlineShow = false;\nlet homeInlineText = null;\nlet homeInlineMarkup = null;\n\nif (!path && shouldSendMenu) {\n homeInlineShow = true;\n\n // Текст под инлайн-кнопками\n homeInlineText = (lang === 'ru')\n ? '🚀 Быстрый старт'\n : '🚀 Quick start';\n\n // Пример кнопок — под себя подправишь\n if (lang === 'ru') {\n homeInlineMarkup = {\n inline_keyboard: [\n [\n { text: '⚡️ FLUX', callback_data: 'home:flux' },\n ],\n [\n { text: '📘 Инструкция', callback_data: 'home:instruction' },\n ],\n [\n { text: '💰 Баланс', callback_data: 'account:balance' },\n { text: '⚡️ Купить кредиты', callback_data: 'account:buy_credits' },\n ],\n ],\n };\n } else {\n homeInlineMarkup = {\n inline_keyboard: [\n [\n { text: '⚡️ FLUX', callback_data: 'home:flux' },\n ],\n [\n { text: '📘 Instructions', callback_data: 'home:instruction' },\n ],\n [\n { text: '💰 Balance', callback_data: 'account:balance' },\n { text: '⚡️ Buy credits', callback_data: 'account:buy_credits' },\n ],\n ],\n };\n }\n}\n\n// ====== Keyboard (v1: hide subscription, keep credits) ======\nlet dynNode = node;\n\n// Мягко скрываем пункт \"buy_sub\" в аккаунте (UI), логика ветки buy_sub остаётся\nif (node.key === 'account') {\n dynNode = {\n ...node,\n children: (node.children || []).filter(c => c.key !== 'buy_sub')\n };\n}\n\n// Для community показываем клавиатуру как на главном экране (root MENU),\n// но текст/placeholder остаются от самого раздела community.\nconst keyboardNode = (node.key === 'community') ? MENU : dynNode;\n\n// reply keyboard (skip when lang inline or account action)\nlet replyMarkup = undefined, replyText = undefined;\nif (!isLangTool && !isAccountAction) {\n const rows=[];\n const webapps=(keyboardNode.children||[]).filter(c=>c.type==='webapp');\n for (const c of webapps) {\n rows.push([{ text: c.title, web_app: { url: c.webUrl } }]);\n }\n\n const normals=(keyboardNode.children||[]).filter(c=>c.type!=='webapp');\n for (let i=0; i<normals.length; i+=2) {\n rows.push(normals.slice(i,i+2).map(c => ({ text: c.title })));\n }\n\n const nav = [];\n // На экране community кнопку Back не показываем,\n // чтобы клавиатура была как у home (только Home).\n const showBack = path && node.key !== 'community';\n if (showBack) {\n nav.push({ text: BTN_BACK });\n }\n nav.push({ text: BTN_MAIN });\n rows.push(nav);\n\n\n replyMarkup = {\n keyboard: rows,\n resize_keyboard: true,\n one_time_keyboard: false,\n selective: false,\n input_field_placeholder: dynNode.title || L('placeholder'),\n };\n replyText = (path ? `${dynNode.title}\\n${dynNode.desc||''}` : L('mainTitle')).trim();\n}\n\n\n\n// prompt fields\nconst promptText = msg.caption ?? msg.text ?? '';\n\n// media\nlet photoFileId=null, file_kind=null, file_name=null, mime_type=null;\nconst doc = msg.document;\nif (doc?.file_id) {\n const mt = String(doc.mime_type || '').toLowerCase();\n const name = String(doc.file_name || '').toLowerCase();\n const looksImage = mt.startsWith('image/') || /\\.(png|jpe?g|webp|bmp|gif|tiff?)$/.test(name);\n if (looksImage) { photoFileId=doc.file_id; file_kind='document'; file_name=doc.file_name||null; mime_type=doc.mime_type||null; }\n}\nif (!photoFileId && Array.isArray(msg.photo) && msg.photo.length>0) {\n const best = msg.photo[msg.photo.length-1];\n photoFileId = best.file_id; file_kind='photo';\n}\n\n// output\nreturn [{\n chatId,\n tool: session.tool,\n account_action: account_action || undefined,\n intent: isLangTool ? 'LANG_PICK' : (isAccountAction ? 'ACCOUNT_ACTION' : (isInstructionTool ? 'INSTRUCTION' : undefined)),\n\n shouldSendMenu,\n isPrompt,\n replyText,\n replyMarkup,\n silent: true,\n prompt: promptText,\n\n photo_file_id: photoFileId,\n file_kind, file_name, mime_type,\n\n deleteChatId: chatId,\n deleteMessageId: msgId,\n path,\n lang,\n is_start: isStartCmd,\n\n // NEW:\n homeInlineShow,\n homeInlineText,\n homeInlineMarkup,\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-96,
2016
],
"id": "a2fa5e5b-86d9-46e8-b5e8-946bee127bcc",
"name": "menu"
},
{
"parameters": {
"jsCode": "// build_sub_invoice — подготовка инвойса Stars (XTR) для подписки Standard\n// ВХОД: ожидаем, что сверху пришло tool:'account' и account_action:'buy_sub' + chatId/msg\n// ВЫХОД: invoice (плоские поля для createInvoiceLink), robokassa_url, тексты UI\n\nconst sd = $getWorkflowStaticData('global');\n\n// --- Вход / контекст\nconst chatId = String($json.chatId || $json.message?.chat?.id || '');\nconst msgId = $json.deleteMessageId || $json.message?.message_id || undefined;\n\nconst tool = $json.tool || null;\nconst action = $json.account_action || null;\n\n// Язык: по входу → кэш → подсказка Telegram → ru\nlet lang = (typeof $json.lang === 'string' && /^(ru|en)$/i.test($json.lang)) \n ? $json.lang.toLowerCase()\n : (sd.userLang?.[chatId] || (String($json.message?.from?.language_code || '').startsWith('en') ? 'en' : 'ru'));\nsd.userLang = sd.userLang || {};\nsd.userLang[chatId] = lang;\n\n// Фильтр: запускаемся только на покупку подписки из меню аккаунта\nif (tool !== 'account' || action !== 'buy_sub') {\n return [{ json: { skip: true, reason: 'not_account_buy_sub' } }];\n}\n\n// --- Конфигурация цены (звёзды = XTR, целое число)\nconst PLAN_KEY = 'standard';\nconst PRICE_STARS = Number(sd.pricing?.sub?.standard_stars) || 1; // TODO: поставь свою цену в звёздах\nconst DURATION_DAYS = 30;\n\n// --- Локализация\nconst T = (k) => {\n const RU = {\n title: 'Подписка Стандарт',\n desc: 'Доступ ко всем инструментам на 30 дней',\n label: `Стандарт (${DURATION_DAYS} дней)`,\n prompt: `Выберите способ оплаты подписки «Стандарт» на ${DURATION_DAYS} дней.\\nСтоимость: ${PRICE_STARS} ⭐️`,\n payStars: 'Оплатить звёздами',\n payCard: 'Оплатить картой'\n };\n const EN = {\n title: 'Standard Subscription',\n desc: 'Access to all tools for 30 days',\n label: `Standard (${DURATION_DAYS} days)`,\n prompt: `Choose a payment method for “Standard” ${DURATION_DAYS}-day plan.\\nPrice: ${PRICE_STARS} ⭐️`,\n payStars: 'Pay with Stars',\n payCard: 'Pay by card'\n };\n const dict = (lang === 'en') ? EN : RU;\n return dict[k] || k;\n};\n\n// --- Параметры для createInvoiceLink (Stars/XTR не требует provider_token)\nconst payload = `sub:${PLAN_KEY}:${chatId}:${Date.now()}`;\n\n// Важно: prices должен быть JSON-строкой массива LabeledPrice ({label, amount})\nconst prices = JSON.stringify([{ label: T('label'), amount: PRICE_STARS }]);\n\nconst invoice = {\n title: T('title'),\n description: T('desc'),\n payload,\n currency: 'XTR',\n prices,\n // Не указываем provider_token для Stars!\n // Дополнительно можно добавить:\n // photo_url: 'https://.../preview.jpg',\n // need_name/address/... — НЕ нужно для Stars\n};\n\n// Плейсхолдер для оплаты картой (Robokassa) — просто ссылка на ваш бэкенд\nconst robokassa_url = `https://pay.example/robokassa?plan=${PLAN_KEY}&uid=${encodeURIComponent(chatId)}`;\n\n// Текст сообщения, которое отправим после получения invoice_link\nconst pay_prompt_text = T('prompt');\n\n// Возвращаем все данные для следующих нод\nreturn [{\n json: {\n chatId,\n // Чтобы удалить исходное сообщение пользователя\n deleteChatId: chatId,\n deleteMessageId: msgId,\n\n // Для следующего шага (HTTP Request -> createInvoiceLink)\n invoice, // плоские поля\n // Для сообщения-кнопок\n robokassa_url,\n pay_prompt_text,\n ui_texts: {\n payStars: T('payStars'),\n payCard: T('payCard')\n },\n\n meta: { tool, action, lang, plan: PLAN_KEY, price_stars: PRICE_STARS, duration_days: DURATION_DAYS }\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2032,
432
],
"id": "7bb0d7a4-a8ec-49f1-b251-7e67d34424bf",
"name": "build_sub_invoice",
"disabled": true
},
{
"parameters": {
"jsCode": "// Ловим и парсим pre_checkout_query (в т.ч. Stars: currency === \"XTR\")\n// Если апдейт не тот — ничего не отдаём.\n\nconst pq = $json.pre_checkout_query;\nif (!pq) {\n return [];\n}\n\n// Базовые поля\nconst id = String(pq.id || '');\nconst fromId = Number(pq.from?.id || 0);\nconst username = String(pq.from?.username || '');\nconst chatId = String(pq.chat?.id || pq.from?.id || ''); // для приватных обычно = from.id\nconst currency = String(pq.currency || ''); // XTR для звёзд\nconst total_amount = Number(pq.total_amount || 0);\nconst payload = String(pq.invoice_payload || ''); // то, что вы закладывали при sendInvoice\n\n// Если нужно — валидация payload (план, срок, и т.д.)\n// Здесь просто прокидываем дальше.\nreturn [{\n json: {\n kind: 'pre_checkout',\n chatId,\n userId: fromId,\n username,\n pre_checkout_query_id: id,\n currency,\n total_amount,\n payload,\n // Решение об ок/не ок можно принимать здесь:\n ok: true,\n error_message: null\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-912,
-1280
],
"id": "d320d2ff-a421-415a-afa1-84f967c24d2e",
"name": "payment.precheckout.parse"
},
{
"parameters": {
"method": "POST",
"url": "https://api.telegram.org/bot8495842221:AAHBriXvzudyazB2f8z3ilGQ8A5ESdjjtl8/createInvoiceLink",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "title",
"value": "={{ $json.invoice.title }}"
},
{
"name": "description",
"value": "={{ $json.invoice.description }}"
},
{
"name": "payload",
"value": "={{ $json.invoice.payload }}"
},
{
"name": "currency",
"value": "={{ $json.invoice.currency }}"
},
{
"name": "prices",
"value": "={{ $json.invoice.prices }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2336,
432
],
"id": "4279494d-0a2d-48eb-af5c-ed01d2b3f370",
"name": "createInvoiceLink",
"disabled": true
},
{
"parameters": {
"method": "POST",
"url": "https://api.telegram.org/bot8495842221:AAHBriXvzudyazB2f8z3ilGQ8A5ESdjjtl8/answerPreCheckoutQuery",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "pre_checkout_query_id",
"value": "={{ $json.pre_checkout_query_id }}"
},
{
"name": "ok",
"value": "={{ $json.ok }}"
},
{
"name": "error_message",
"value": "={{ $json.error_message }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-640,
-1280
],
"id": "b50f9952-0532-40f3-ad19-7c47386d768e",
"name": "answerPreCheckoutQuery"
},
{
"parameters": {
"jsCode": "/**\n * Parse Telegram successful_payment → DB payload + ACK\n * Подключение:\n * 1) HTTP → Supabase (используй $json.db_event)\n * 2) Telegram → sendMessage (chat_id = $json.ack.chat_id, text = $json.ack.text)\n */\n\nconst sd = $getWorkflowStaticData('global');\nsd.userLang = sd.userLang || {};\nsd.payments = sd.payments || {}; // идемпотентность по charge_id\n\n// ---- Input\nconst upd = $json || {};\nconst msg = upd.message || {};\nconst sp = msg.successful_payment || {};\n\nconst chatId = String(msg.chat?.id || '');\nconst userId = Number(msg.from?.id || chatId || 0);\nconst uname = String(msg.from?.username || '');\nconst fname = String([msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(' ') || '');\nconst lang = (sd.userLang[chatId] || (/^en/i.test(String(msg.from?.language_code||'')) ? 'en' : 'ru'));\n\nconst currency = String(sp.currency || 'XTR');\nconst amount = Number(sp.total_amount || 0);\nconst payload_raw = String(sp.invoice_payload || '');\nconst tg_charge_id = String(sp.telegram_payment_charge_id || '');\nconst provider_charge_id = String(sp.provider_payment_charge_id || '');\nconst paid_at = new Date().toISOString();\n\n// ---- Safe date utils\nfunction addDays(baseIso, days){\n const base = baseIso ? new Date(baseIso) : new Date();\n if (!Number.isFinite(days)) return null;\n if (isNaN(base.getTime())) return null;\n base.setUTCDate(base.getUTCDate() + days);\n try { return base.toISOString(); } catch { return null; }\n}\nfunction toHuman(iso, lang){\n if (!iso) return '—';\n const d = new Date(iso);\n if (isNaN(d.getTime())) return '—';\n try {\n const loc = lang === 'en' ? 'en' : 'ru';\n return d.toLocaleString(loc, {\n year:'numeric', month:'2-digit', day:'2-digit',\n hour:'2-digit', minute:'2-digit'\n });\n } catch {\n const pad = n => String(n).padStart(2,'0');\n return `${d.getUTCFullYear()}-${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())} UTC`;\n }\n}\n\n// ---- Parse payload (сохраняем nonce, но не используем его для срока)\nfunction parsePayload(raw){\n const out = { raw, type:'unknown', plan:null, credits:null, user:null, extra:{} };\n if (!raw) return out;\n const parts = String(raw).split(':');\n const head = (parts[0]||'').toLowerCase();\n\n if (['sub','subscription'].includes(head)) {\n out.type = 'subscription';\n out.plan = parts[1] || 'standard';\n out.user = Number(parts[2] || userId) || null;\n out.extra.nonce = parts[3] || null; // timestamp/nonce из payload, НЕ срок\n } else if (['credits','cr'].includes(head)) {\n out.type = 'credits';\n const maybe = parts[1] || '';\n const n = Number(maybe);\n out.credits = Number.isFinite(n) && n>0 ? n : (parseInt(String(maybe).replace(/\\D+/g,''),10) || null);\n out.plan = parts[1] || 'pack';\n }\n return out;\n}\nconst pp = parsePayload(payload_raw);\n\n// ---- Idempotency\nlet alreadyProcessed = false;\nif (tg_charge_id) {\n if (sd.payments[tg_charge_id]) alreadyProcessed = true;\n sd.payments[tg_charge_id] = true;\n}\n\n// ---- Business rules (жёстко: подписка = 30 дней и +270 кредитов)\nlet product_kind = pp.type; // 'subscription' | 'credits' | 'unknown'\nlet product_code = pp.plan || 'standard';\nlet term_days = null;\nlet credits_add = pp.credits;\n\nif (product_kind === 'subscription') {\n term_days = 30; // фиксированный срок\n credits_add = 270; // фиксированное начисление\n}\n\n// Ожидаемая дата окончания\nconst expected_until_iso = (product_kind === 'subscription' && term_days)\n ? addDays(paid_at, term_days)\n : null;\n\n// ---- DB payload\nconst db_event = {\n tg_user_id: userId,\n chat_id: chatId,\n username: uname,\n full_name: fname,\n currency,\n amount,\n payload_raw,\n payload_type: product_kind,\n plan: product_code,\n term: null,\n term_days: term_days,\n credits: credits_add,\n lang,\n telegram_payment_charge_id: tg_charge_id,\n provider_payment_charge_id: provider_charge_id,\n paid_at,\n expected_until: expected_until_iso\n};\n\n// ---- ACK text\nlet ackLines = [];\nif (lang === 'en') {\n ackLines.push(alreadyProcessed ? 'ℹ️ Payment was already processed.' : '✅ Payment received.');\n if (product_kind === 'subscription') {\n ackLines.push(`Type: subscription (${product_code})`);\n ackLines.push(`Term: ${term_days} days`);\n ackLines.push(`Credits: +${credits_add}`);\n if (expected_until_iso) ackLines.push(`Valid till (expected): ${toHuman(expected_until_iso,'en')}`);\n } else if (product_kind === 'credits') {\n ackLines.push(`Type: credits`);\n ackLines.push(`Credits: +${credits_add ?? '—'}`);\n } else {\n ackLines.push(`Type: ${product_kind}`);\n }\n ackLines.push(`Amount: ${amount} ${currency}`);\n if (tg_charge_id) ackLines.push(`Receipt: ${tg_charge_id}`);\n} else {\n ackLines.push(alreadyProcessed ? 'ℹ️ Платёж уже был обработан.' : '✅ Оплата получена.');\n if (product_kind === 'subscription') {\n ackLines.push(`Тип: подписка (${product_code})`);\n ackLines.push(`Срок: ${term_days} дн.`);\n ackLines.push(`Кредиты: +${credits_add}`);\n if (expected_until_iso) ackLines.push(`Действует до (ожид.): ${toHuman(expected_until_iso,'ru')}`);\n } else if (product_kind === 'credits') {\n ackLines.push(`Тип: кредиты`);\n ackLines.push(`Кредиты: +${credits_add ?? '—'}`);\n } else {\n ackLines.push(`Тип: ${product_kind}`);\n }\n ackLines.push(`Сумма: ${amount} ${currency}`);\n if (tg_charge_id) ackLines.push(`Чек: ${tg_charge_id}`);\n}\n\nconst ack = {\n chat_id: chatId,\n text: ackLines.join('\\n'),\n disable_notification: true,\n};\n\n// ---- Return\nreturn [{\n json: {\n ok: true,\n kind: 'payment_success',\n chatId,\n userId,\n lang,\n db_event,\n ack\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-912,
-1072
],
"id": "f971aae9-adcd-460e-b37d-5e94e6a8e6f7",
"name": "payment.parse_success"
},
{
"parameters": {
"method": "POST",
"url": "=https://bwbsclwdkighhzlyiman.supabase.co/rest/v1/rpc/apply_telegram_payment",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": " application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"p_tg_user_id\": {{ $json.userId }},\n \"p_provider\": \"telegram-stars\",\n \"p_charge_id\": \"{{ $json.db_event.provider_payment_charge_id }}\",\n \"p_currency\": \"{{ $json.db_event.currency }}\",\n \"p_amount\": {{ $json.db_event.amount }},\n \"p_product_kind\": \"{{ $json.db_event.payload_type || 'unknown' }}\",\n \"p_product_code\": \"{{ $json.db_event.plan || '' }}\",\n \"p_credits_added\": {{ $json.db_event.credits || 0 }},\n \"p_plan\": \"{{ $json.db_event.payload_type === 'subscription'\n ? ($json.db_event.plan || 'standard')\n : null }}\",\n \"p_plan_days\": {{ $json.db_event.payload_type === 'subscription'\n ? ($json.db_event.term_days || 0)\n : 0 }},\n \"p_lang\": \"{{ $json.lang || $json.db_event.lang || 'ru' }}\"\n}\n\n",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-640,
-1072
],
"id": "e9bb008d-dcb9-4b6b-bc08-f67041f48bb1",
"name": "apply_payment",
"credentials": {
"supabaseApi": {
"id": "jGgpXKPYHiL193Rz",
"name": "Supabase account"
}
}
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "58c08747-c6af-4557-9934-fd5bcba58813",
"leftValue": "={{ $json.pre_checkout_query }}",
"rightValue": "",
"operator": {
"type": "object",
"operation": "exists",
"singleValue": true
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d0bf0e9a-81b2-4be3-b897-f8b1c405c5c0",
"leftValue": "={{ $json.message.successful_payment }}",
"rightValue": "",
"operator": {
"type": "object",
"operation": "exists",
"singleValue": true
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.callback_query }}",
"rightValue": "gpt",
"operator": {
"type": "object",
"operation": "exists",
"singleValue": true
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.callback_query }}",
"rightValue": "=flux",
"operator": {
"type": "object",
"operation": "notExists",
"singleValue": true
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
-1328,
1920
],
"id": "d8fd6414-7431-4c53-9815-32a6e54887bf",
"name": "Switch_message_type"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.message.web_app_data }}",
"rightValue": "gpt",
"operator": {
"type": "object",
"operation": "exists",
"singleValue": true
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.message.web_app_data }}",
"rightValue": "=flux",
"operator": {
"type": "object",
"operation": "notExists",
"singleValue": true
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
-320,
2016
],
"id": "878f23f7-8821-4e12-8edd-7034e1b4de40",
"name": "Switch_web_app"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.shouldSendMenu }}",
"rightValue": "gpt",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.shouldSendMenu }}",
"rightValue": "=flux",
"operator": {
"type": "boolean",
"operation": "false",
"singleValue": true
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
400,
2016
],
"id": "c7049219-c441-46c6-a96f-d06cd341662d",
"name": "switch_reply_menu"
},
{
"parameters": {
"jsCode": "// FIRST FUNCTION: parse web_app_data & save settings (FLUX + SEEDANCE + SEEDREAM + VEO + SORA)\nconst sd = $getWorkflowStaticData('global');\nsd.flux = sd.flux || {}; // { [chatId]: { ar, ref, seed, quality, useRef, savedAt } }\nsd.seedance = sd.seedance || {}; // { [chatId]: { mode,tier,model,i2v_mode,resolution,ratio,duration,camerafixed,savedAt } }\nsd.seedream = sd.seedream || {}; // { [chatId]: { quality, ratio, dims_t2i:{w,h}, long_edge_i2i, savedAt } }\nsd.veo = sd.veo || {}; // { [chatId]: { model, ratio, mode, savedAt } }\nsd.sora = sd.sora || {}; // { [chatId]: { model, ratio, duration, savedAt } } <-- NEW\n\nconst msg = $json.message || {};\nconst chatId = String(msg.chat?.id || $json.chat?.id || '');\nconst wad = msg.web_app_data?.data; // JSON-строка из Telegram WebApp\n\nlet saved = false;\nlet kind = null; // 'flux' | 'seedance' | 'seedream' | 'veo' | 'sora'\nlet savedCfg = null;\nlet err = null;\n\nfunction clamp(n, lo, hi) { return Math.min(hi, Math.max(lo, n)); }\n\ntry {\n if (chatId && wad) {\n const raw = JSON.parse(wad);\n\n // ======== FLUX ========\n if (raw?.type === 'flux_settings') {\n kind = 'flux';\n\n const cleanAr = String(raw.ar ?? '1:1').replace(/\\s+/g, '');\n const ar = /^\\d+:\\d+$/.test(cleanAr) ? cleanAr : '1:1';\n\n let ref = Number(raw.ref);\n if (!Number.isFinite(ref)) ref = 0.75;\n ref = clamp(ref, 0, 1.2);\n\n const seed = raw.random ? 0 : (Number(raw.seed) || 0);\n const qNum = Number(raw.quality);\n const bNum = Number(raw.batch);\n const quality = [0,1,2].includes(qNum) ? qNum : 1;\n const batch = [1,2,4].includes(bNum) ? bNum : 1;\n \n const useRef = Boolean(raw.useRef ?? raw.use_ref ?? false);\n\n const cfg = { ar, ref, seed, quality, batch, useRef, savedAt: new Date().toISOString() };\n sd.flux[chatId] = cfg;\n saved = true;\n savedCfg = cfg;\n }\n\n // ======== SEEDANCE ========\n if (raw?.type === 'seedance_settings') {\n kind = 'seedance';\n\n const mode = /^(i2v|t2v)$/.test(String(raw.mode)) ? raw.mode : 'i2v';\n const tier = /^(lite|pro)$/.test(String(raw.tier)) ? raw.tier : 'lite';\n\n const i2v_mode = /^(first|last|first_last|reference)$/.test(String(raw.i2v_mode))\n ? raw.i2v_mode : 'first';\n\n const MODEL_IDS = {\n lite: {\n i2v: 'bytedance-seedance-1-0-lite-i2v-250428',\n t2v: 'bytedance-seedance-1-0-lite-t2v-250428',\n },\n pro: {\n i2v: null,\n t2v: null,\n }\n };\n const fallbackModel = MODEL_IDS?.[tier]?.[mode] || null;\n const model = String(raw.model || fallbackModel || '');\n\n const resolution = /^(480p|720p)$/i.test(String(raw.resolution)) ? raw.resolution.toLowerCase() : '480p';\n\n const R = new Set(['auto','16:9','4:3','1:1','3:4','9:16','21:9']);\n const ratio = R.has(String(raw.ratio)) ? String(raw.ratio) : 'auto';\n\n let duration = Number(raw.duration);\n duration = (duration === 5 || duration === 10) ? duration : 5;\n\n const camerafixed = Boolean(raw.camerafixed);\n\n const cfg = {\n mode, tier, model,\n i2v_mode,\n resolution, ratio, duration, camerafixed,\n savedAt: new Date().toISOString()\n };\n sd.seedance[chatId] = cfg;\n saved = true;\n savedCfg = cfg;\n }\n\n // ======== SEEDREAM (image gen) ========\n if (raw?.type === 'seedream_settings') {\n kind = 'seedream';\n\n let quality = String(raw.quality || '2k').toLowerCase();\n if (!['1k','2k','4k'].includes(quality)) quality = '2k';\n\n const R2 = new Set(['1:1','4:3','3:4','16:9','9:16','3:2','2:3','21:9']);\n let ratio = String(raw.ratio || '1:1');\n if (!R2.has(ratio)) ratio = '1:1';\n\n const BASE_2K = {\n '1:1': { w: 2048, h: 2048 },\n '4:3': { w: 2304, h: 1728 },\n '3:4': { w: 1728, h: 2304 },\n '16:9': { w: 2560, h: 1440 },\n '9:16': { w: 1440, h: 2560 },\n '3:2': { w: 2496, h: 1664 },\n '2:3': { w: 1664, h: 2496 },\n '21:9': { w: 3024, h: 1296 },\n };\n\n const scale = (quality === '1k') ? 0.5 : (quality === '4k') ? 2 : 1;\n const base = BASE_2K[ratio] || BASE_2K['1:1'];\n const dims_t2i = { w: Math.round(base.w * scale), h: Math.round(base.h * scale) };\n const long_edge_i2i = (quality === '1k') ? 1024 : (quality === '4k' ? 4096 : 2048);\n\n const cfg = { quality, ratio, dims_t2i, long_edge_i2i, savedAt: new Date().toISOString() };\n sd.seedream[chatId] = cfg;\n saved = true;\n savedCfg = cfg;\n }\n\n // ======== VEO (video gen) ========\n if (raw?.type === 'veo_settings') {\n kind = 'veo';\n\n let model = String(raw.model || 'veo3.1').toLowerCase();\n if (!/^veo/.test(model)) model = 'veo3.1';\n\n let ratio = String(raw.ratio || '').trim();\n if (!/^(16:9|9:16)$/.test(ratio)) ratio = 'auto';\n\n let mode = String(raw.mode || 't2v');\n if (!/^(t2v|i2v|first_last|reference)$/.test(mode)) mode = 't2v';\n\n const cfg = { model, ratio, mode, savedAt: new Date().toISOString() };\n sd.veo[chatId] = cfg;\n saved = true;\n savedCfg = cfg;\n }\n\n // ======== SORA (video gen) — NEW ========\n if (raw?.type === 'sora_settings') {\n kind = 'sora';\n\n // model: sora-2-pro | sora-2-hd | sora-2\n const ALLOWED = new Set(['sora-2-pro','sora-2-hd','sora-2']);\n let model = String(raw.model || 'sora-2').toLowerCase();\n if (!ALLOWED.has(model)) model = 'sora-2';\n\n // ratio: 16:9 | 9:16\n let ratio = String(raw.ratio || '16:9');\n if (!/^(16:9|9:16)$/.test(ratio)) ratio = '16:9';\n\n // duration: 4 | 8 | 12\n let duration = Number(raw.duration);\n duration = [4,8,12].includes(duration) ? duration : 8;\n\n const cfg = { model, ratio, duration, savedAt: new Date().toISOString() };\n sd.sora[chatId] = cfg;\n saved = true;\n savedCfg = cfg;\n }\n }\n} catch (e) {\n err = String(e?.message || e);\n}\n\nreturn [{\n json: {\n chatId,\n hasWebAppData: Boolean(wad),\n saved,\n kind, // 'flux' | 'seedance' | 'seedream' | 'veo' | 'sora' | null\n savedCfg,\n errorMsg: err || null,\n deleteChatId: chatId,\n deleteMessageId: msg.message_id\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-96,
1808
],
"id": "bfc9c533-bb3d-465c-b2c4-e97efae0a3bf",
"name": "parse web_app_data"
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.lang === 'en' ? '🌐 Choose your language:' : '🌐 Выберите язык:' }}",
"replyMarkup": "inlineKeyboard",
"inlineKeyboard": {
"rows": [
{
"row": {
"buttons": [
{
"text": "Русский ",
"additionalFields": {
"callback_data": "setlang:ru"
}
},
{
"text": "English ",
"additionalFields": {
"callback_data": "setlang:en"
}
}
]
}
}
]
},
"additionalFields": {
"appendAttribution": false
}
},
"id": "42b24f4b-6d0b-4bf9-b679-15699f28dac9",
"name": "lang_inline",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2016,
992
],
"webhookId": "2b9cbcf8-d73a-4d63-ba56-8fac5109a862",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $json.result.chat.id }}",
"messageId": "={{ $('Switch1').item.json.deleteMessageId }}"
},
"id": "d7a6d677-9096-4f04-af9b-6cbf64816edb",
"name": "delMes",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2224,
992
],
"webhookId": "7e67278c-ca08-4b2b-ade0-5fbc6c1e9c14",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $('Switch1').item.json.chatId }}",
"messageId": "={{ $('Switch1').item.json.deleteMessageId }}"
},
"id": "6cff3a8a-d1ce-4931-a54d-84be2051dd0b",
"name": "delMes1",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2576,
176
],
"webhookId": "72e170d1-8ab7-402e-84e2-57ed36c830ef",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $json.chatId }}",
"messageId": "={{ $json.targetMsgId }}"
},
"id": "c5fc35e3-5029-49ff-bdbf-4e40b5ed2c5a",
"name": "delMes_targetMsg",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-448,
-480
],
"webhookId": "263a1d6e-2aab-45a0-ac0a-fd4f61fe227d",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $json.chatId }}",
"messageId": "={{ $json.keyboardMsgId }}"
},
"id": "78fbf151-84d7-4f87-9277-73c25acf66ae",
"name": "delMes_keyboardMsg",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-240,
-480
],
"webhookId": "bfdf4554-b218-4367-b05c-4a3a963e8692",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $('message_reply_inline').item.json.chatId }}",
"messageId": "={{ $('message_reply_inline').item.json.keyboardMsgId }}"
},
"id": "cdd2a43d-31ab-464e-970c-78ffcfbfa81f",
"name": "delMes_keyboardMsg1",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-240,
32
],
"webhookId": "e7190172-8910-40d7-8c25-51328f01d3d9",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "78542b6d-15c2-4809-935c-49d1e17dcc1e",
"leftValue": "={{ $json.account_action }}",
"rightValue": "balance",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "dfbae9da-3cc7-488e-9f71-f21a4b9b4345",
"leftValue": "={{ $json.account_action }}",
"rightValue": "status",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.account_action }}",
"rightValue": "buy_sub",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.account_action }}",
"rightValue": "=buy_credits",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
1760,
304
],
"id": "109d52d5-e6c4-4550-8072-79dd6e29ae30",
"name": "Switch_lang_act"
},
{
"parameters": {
"method": "POST",
"url": "https://api.telegram.org/bot8495842221:AAHBriXvzudyazB2f8z3ilGQ8A5ESdjjtl8/deleteMessage",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "chat_id",
"value": "={{ $('menu').item.json.chatId }}"
},
{
"name": "message_id",
"value": "={{ $('menu').item.json.deleteMessageId }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
816,
1792
],
"id": "21afacf0-2d14-4d4a-a3a3-db692b94eec7",
"name": "deleteMessageMenu",
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const sd = $getWorkflowStaticData('global');\nsd.userLang = sd.userLang || {};\n\n// Инлайн-колбэк от Telegram\nconst cq = $json.callback_query || {};\nconst dataRaw = cq.data;\nconst data = typeof dataRaw === 'string'\n ? dataRaw.trim()\n : String(dataRaw || '').trim();\n\nconst chatId = String(cq.message?.chat?.id || '');\nconst keyboardMsgId = cq.message?.message_id; // наше сообщение с инлайнами\n\nlet action = null; // 'del_one' | 'del_all' | 'setlang' | 'buy_credits_pack'\nlet targetMsgId = null; // для del_one\nlet pack_credits = null; // для buy_credits_pack\nlet lang = sd.userLang[chatId] || null; // 'ru' | 'en', если уже выбран язык\n\n// ---------- разбор callback_data ----------\n// Форматы, которые сейчас используются:\n// - \"del:12345\" → удалить одно сообщение\n// - \"del_all\" → очистить все\n// - \"setlang:ru|en\" → смена языка\n// - \"credits:200\" → выбор пакета кредитов (200/500/1000)\n\nif (data.startsWith('del:')) {\n // Удалить одно сообщение\n action = 'del_one';\n const idStr = data.split(':')[1];\n const n = Number(idStr);\n if (Number.isFinite(n)) targetMsgId = n;\n\n} else if (data === 'del_all') {\n // Удалить все\n action = 'del_all';\n\n} else if (data.startsWith('setlang:')) {\n // Смена языка\n action = 'setlang';\n const tail = data.split(':')[1]?.trim().toLowerCase();\n if (tail === 'ru' || tail === 'en') {\n lang = tail;\n }\n\n} else if (data.startsWith('credits:')) {\n // Выбор пакета кредитов\n // callback_data: \"credits:200\" | \"credits:500\" | \"credits:1000\"\n action = 'buy_credits_pack';\n const tail = data.split(':')[1]?.trim();\n const n = Number(tail);\n if (Number.isFinite(n) && n > 0) {\n pack_credits = n;\n }\n}\n\n// Валидируем язык: по умолчанию 'ru'\nif (!['ru', 'en'].includes(lang)) {\n lang = 'ru';\n}\n\n// Подстраховка для del_one — если id не пришёл в callback_data,\n// попробуем взять из reply_to_message\nif (!targetMsgId && cq.message?.reply_to_message?.message_id) {\n targetMsgId = cq.message.reply_to_message.message_id;\n}\n\nreturn [{\n json: {\n action, // 'del_one' | 'del_all' | 'setlang' | 'buy_credits_pack'\n lang, // язык (особенно важен при setlang)\n chatId,\n targetMsgId, // для del_one\n keyboardMsgId, // id сообщения с инлайн-кнопками\n callback_query_id: cq.id,\n pack_credits // для buy_credits_pack (200/500/1000 или null)\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-912,
-192
],
"id": "849ffd5b-8842-4b3a-83c4-7a6e85965b67",
"name": "message_reply_inline"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.action }}",
"rightValue": "del_one",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.action }}",
"rightValue": "=del_all",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "df33815a-03c8-4a3a-a25f-0a4a7e266f1a",
"leftValue": "={{ $json.action }}",
"rightValue": "setlang",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "fed3019b-9611-47cc-969b-c87ddce299c7",
"leftValue": "={{ $json.action }}",
"rightValue": "buy_credits_pack",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
-704,
-224
],
"id": "1ff60860-9caf-4e9c-8cf7-af9dc606b8f6",
"name": "Switch_inline_act"
},
{
"parameters": {
"chatId": "={{ $('payment.parse_success').item.json.chatId }}",
"text": "={{ $('payment.parse_success').item.json.ack.text }}",
"additionalFields": {
"appendAttribution": false,
"parse_mode": "HTML"
}
},
"id": "5713ca55-a40c-4c5e-a773-37ef474b0840",
"name": "send_mes_payment",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-384,
-1072
],
"webhookId": "c3fe8895-404b-4fa5-a0c8-46733c06592f",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://bwbsclwdkighhzlyiman.supabase.co/rest/v1/rpc/set_user_lang",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": " application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"p_tg_user_id\": {{ $json.chatId }},\n \"p_lang\": \"{{ $json.lang }}\"\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-464,
32
],
"id": "8e6d0bcf-fa72-46fb-bf19-3aadda914ce2",
"name": "SB_set_user_lang",
"credentials": {
"supabaseApi": {
"id": "jGgpXKPYHiL193Rz",
"name": "Supabase account"
}
}
},
{
"parameters": {
"jsCode": "// n8n Code — Build items-to-delete (UNIFIED) — same chatId resolver as \"Clear ALL\"\n// Output: array of { json: { chatId, messageId } } in this order:\n// 1) sd.replyIndex[chatId] (+ keyboardMsgId if provided)\n// 2) sd.photoBucket[chatId].files[].message_id (+ targetMsgId if provided)\n// 3) keys of sd.photoIndex[chatId]\n\nconst sd = $getWorkflowStaticData('global');\n\n// Ensure unified stores (do NOT mutate here)\nsd.photoBucket = sd.photoBucket || {}; // { [chatId]: { files:[{file_id,message_id,...}], savedAt } }\nsd.photoIndex = sd.photoIndex || {}; // { [chatId]: { [messageId]: file_id } }\nsd.replyIndex = sd.replyIndex || {}; // { [chatId]: number[] }\n\n// Same chatId resolution as in your \"Clear ALL\" node\nconst first = $input.first()?.json || {};\nconst msg = $json.message || first.message || {};\nconst chatId = String(\n $json.chatId ||\n first.chatId ||\n msg.chat?.id ||\n $json.chat?.id ||\n first.chat?.id ||\n ''\n);\n\nif (!chatId) {\n return []; // nothing to delete\n}\n\n// Optional context ids from current item\nconst keyboardMsgId = Number($json.keyboardMsgId ?? first.keyboardMsgId ?? 0);\nconst targetMsgId = Number($json.targetMsgId ?? first.targetMsgId ?? 0);\n\n// Helpers\nconst isValidId = (n) => Number.isFinite(n) && n > 0;\nconst uniqOrdered = (arr) => {\n const seen = new Set(); const out = [];\n for (const n of arr) { const v = Number(n); if (isValidId(v) && !seen.has(v)) { seen.add(v); out.push(v); } }\n return out;\n};\n\n// 1) inline replies (replyIndex)\nlet replyIds = Array.isArray(sd.replyIndex[chatId]) ? sd.replyIndex[chatId].slice() : [];\nif (keyboardMsgId) replyIds.push(keyboardMsgId);\nreplyIds = uniqOrdered(replyIds);\n\n// 2) photo message ids from bucket + index\nconst bucketIds = uniqOrdered((sd.photoBucket[chatId]?.files || []).map(x => x?.message_id));\nconst indexIds = uniqOrdered(Object.keys(sd.photoIndex[chatId] || {}));\nlet photoIds = uniqOrdered([...bucketIds, ...indexIds]);\nif (targetMsgId) photoIds = uniqOrdered([...photoIds, targetMsgId]);\n\n// 3) final order: replies first, then photos\nconst all = [...replyIds, ...photoIds];\n\n// Map to items for Telegram deleteMessage\nreturn all.map(messageId => ({ json: { chatId, messageId } }));\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-464,
-208
],
"id": "f02cf1bb-4e6d-44bf-87f6-9f851e828d0b",
"name": "Build items-to-delete"
},
{
"parameters": {
"jsCode": "// Delete ONE image (unified cache)\n// Inputs: chatId (string), targetMsgId (number), keyboardMsgId? (number)\n// Uses: sd.photoBucket, sd.photoIndex, sd.replyIndex\n\nconst sd = $getWorkflowStaticData('global');\nsd.photoBucket = sd.photoBucket || {};\nsd.photoIndex = sd.photoIndex || {};\nsd.replyIndex = sd.replyIndex || {};\n\nconst chatId = String($json.chatId || '');\nconst targetMsgId = Number($json.targetMsgId || 0);\nconst keyboardMsgId = Number($json.keyboardMsgId || 0);\n\nif (!chatId || !Number.isFinite(targetMsgId) || targetMsgId <= 0) {\n return [{ json: { ok:false, reason:'missing_chat_or_target', chatId, targetMsgId } }];\n}\n\n// 1) убрать наш inline-reply из индекса (если есть)\nif (keyboardMsgId && Array.isArray(sd.replyIndex[chatId])) {\n sd.replyIndex[chatId] = sd.replyIndex[chatId].filter(id => id !== keyboardMsgId);\n}\n\n// 2) найти file_id по sd.photoIndex либо по бакету\nconst bucket = sd.photoBucket[chatId];\nconst idxMap = sd.photoIndex[chatId] || {};\nlet removedFileId = idxMap[targetMsgId] || null;\n\nif (!removedFileId && bucket?.files?.length) {\n const hit = bucket.files.find(x => Number(x?.message_id) === targetMsgId);\n if (hit && hit.file_id) removedFileId = hit.file_id;\n}\n\n// 3) удалить из бакета по message_id (и на всякий — по совпадающему file_id)\nlet removed = false;\nif (bucket?.files?.length) {\n const before = bucket.files.length;\n bucket.files = bucket.files.filter(x => {\n if (!x || typeof x !== 'object') return true;\n if (Number(x.message_id) === targetMsgId) return false;\n if (removedFileId && x.file_id === removedFileId) return false;\n return true;\n });\n removed = bucket.files.length < before;\n sd.photoBucket[chatId] = bucket;\n}\n\n// 4) удалить из индекса message_id→file_id (только эту запись)\nif (sd.photoIndex?.[chatId]?.[targetMsgId]) {\n delete sd.photoIndex[chatId][targetMsgId];\n}\n\nreturn [{\n json: {\n ok: true,\n chatId,\n removed,\n removed_file_id: removedFileId || null,\n targetMsgId,\n keyboardMsgId,\n left_in_bucket: sd.photoBucket?.[chatId]?.files?.length || 0\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-48,
-480
],
"id": "068041d9-051b-4218-b482-b6af609ec05b",
"name": "Delete ONE image from memory"
},
{
"parameters": {
"jsCode": "// n8n Code — Clear ALL unified caches for this chat\n// Targets ONLY the unified stores you decided to keep:\n// sd.photoBucket[chatId] — remove\n// sd.photoIndex[chatId] — remove\n// sd.replyIndex[chatId] — remove\n// Leaves sd.poll intact. No legacy/tool-specific caches touched.\n\nconst sd = $getWorkflowStaticData('global');\n\n// Ensure unified stores exist\nsd.photoBucket = sd.photoBucket || {}; // { [chatId]: { files:[{file_id,message_id,...}], savedAt } }\nsd.photoIndex = sd.photoIndex || {}; // { [chatId]: { [messageId]: file_id } }\nsd.replyIndex = sd.replyIndex || {}; // { [chatId]: number[] }\nsd.poll = sd.poll || {};\n\nconst first = $input.first()?.json || {};\nconst msg = $json.message || first.message || {};\nconst chatId = String(\n $json.chatId ||\n first.chatId ||\n msg.chat?.id ||\n $json.chat?.id ||\n first.chat?.id ||\n ''\n);\n\nlet lang = sd.userLang[chatId] || null;\n\nif (!chatId) {\n return [{ json: { ok:false, reason:'no_chat' } }];\n}\n\n// Snapshot BEFORE\nconst before = {\n bucket_files: sd.photoBucket?.[chatId]?.files?.length || 0,\n index_keys: sd.photoIndex?.[chatId] ? Object.keys(sd.photoIndex[chatId]).length : 0,\n reply_len: Array.isArray(sd.replyIndex?.[chatId]) ? sd.replyIndex[chatId].length : 0,\n};\n\n// (optional) collect ids we’re about to drop — handy for downstream debug/logging\nconst bucket_msg_ids = (sd.photoBucket?.[chatId]?.files || [])\n .map(x => Number(x?.message_id))\n .filter(n => Number.isFinite(n) && n > 0);\n\nconst index_msg_ids = sd.photoIndex?.[chatId]\n ? Object.keys(sd.photoIndex[chatId]).map(n => Number(n)).filter(n => Number.isFinite(n) && n > 0)\n : [];\n\nconst reply_msg_ids = Array.isArray(sd.replyIndex?.[chatId]) ? sd.replyIndex[chatId].slice() : [];\n\n// CLEAR unified caches\ndelete sd.photoBucket[chatId];\ndelete sd.photoIndex[chatId];\ndelete sd.replyIndex[chatId];\n\n// Snapshot AFTER\nconst after = {\n bucket_files: sd.photoBucket?.[chatId]?.files?.length || 0,\n index_keys: sd.photoIndex?.[chatId] ? Object.keys(sd.photoIndex[chatId]).length : 0,\n reply_len: Array.isArray(sd.replyIndex?.[chatId]) ? sd.replyIndex[chatId].length : 0,\n};\n\nreturn [{\n json: {\n ok: true,\n lang,\n chatId,\n before,\n after,\n deleted: {\n bucket_msg_ids,\n index_msg_ids,\n reply_msg_ids,\n }\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-240,
-304
],
"id": "0bbabd82-70ab-4f2b-b5b9-e87df2c4f8aa",
"name": "Clear ALL buckets/indexes"
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $json.chatId }}",
"messageId": "={{ $json.messageId }}"
},
"id": "c1ae42f6-0d9f-4aed-b36e-8b10b7ad493d",
"name": "delMes2",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-240,
-144
],
"webhookId": "5becca08-9fa3-416f-8f0c-6e1a114ab9d6",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.lang === 'en'\n ? 'All uploaded photos have been cleared.'\n : 'Все загруженные фото сброшены.'\n}}",
"additionalFields": {
"appendAttribution": false
}
},
"id": "58a2d743-760c-4809-98bc-6f0cfdffc376",
"name": "send_mes_clear",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-48,
-304
],
"webhookId": "a46e0bb1-ac2c-43fc-b456-ef78749654ee",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"chatId": "={{ $('message_reply_inline').item.json.chatId }}",
"text": "={{ $json.lang === 'en'\n ? 'Language saved: English'\n : 'Язык сохранён: Русский'\n }}",
"additionalFields": {
"appendAttribution": false
}
},
"id": "e372a0a1-50a9-4d07-a9a5-113c54b6dd3a",
"name": "send_mes_lang_save",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-48,
32
],
"webhookId": "6f095c52-076a-4679-baf4-721e13505ded",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"chatId": "={{ $json.tg_user_id }}",
"text": "={{ $('Switch_lang_act').item.json.lang === 'en'\n ? 'Current balance: ' + $json.balance + ' credits'\n : 'Текущий баланс вашего аккаунта - ' + $json.balance + ' Кредитов'\n }}",
"additionalFields": {
"appendAttribution": false
}
},
"id": "1585199c-9601-4634-ad56-717376a72cf1",
"name": "send_mes_balance",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2336,
48
],
"webhookId": "92ceebf8-b19e-467b-b2ae-b534a4ea67c3",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.lang === 'en'\n ? 'Current plan: ' + ($json.plan || 'no active plan')\n : 'Текущий статус вашего аккаунта - ' + ($json.plan || 'нет активного плана')\n }}",
"additionalFields": {
"appendAttribution": false
}
},
"id": "8edbccd4-7d68-417c-b90a-c65af0c90abb",
"name": "send_mes_plan",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2336,
256
],
"webhookId": "2a56c513-6002-466f-b3a6-4266d73a69f7",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"chatId": "={{ $('build_sub_invoice').item.json.chatId }}",
"text": "={{ $('build_sub_invoice').item.json.pay_prompt_text }}",
"replyMarkup": "inlineKeyboard",
"inlineKeyboard": {
"rows": [
{
"row": {
"buttons": [
{
"text": "={{ $('build_sub_invoice').item.json.ui_texts.payStars }}",
"additionalFields": {
"url": "={{ $json.result }}"
}
}
]
}
},
{
"row": {
"buttons": [
{
"text": "={{ $('build_sub_invoice').item.json.ui_texts.payCard }}",
"additionalFields": {
"url": "={{ $('build_sub_invoice').item.json.robokassa_url }}"
}
}
]
}
}
]
},
"additionalFields": {
"appendAttribution": false
}
},
"id": "593cdd9f-b109-4b66-ae63-359df315b547",
"name": "send_mes_inline_buy_plan",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2576,
432
],
"webhookId": "e2cbaf9c-00fb-4030-aa3c-6934b8c7a060",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
},
"disabled": true
},
{
"parameters": {
"jsCode": "// n8n Code — Build \"choose credits pack\" message\n// Вход: $json с полями lang, chatId (из меню/роутера)\n// Выход: chatId, lang, text, reply_markup.inline_keyboard\n\nconst msg = $json.message || {};\nconst chatId = String(\n $json.chatId ||\n msg.chat?.id ||\n $json.chat?.id ||\n ''\n);\n\n// fallback, как в menu/аккаунте\nlet lang = (typeof $json.lang === 'string' && $json.lang.toLowerCase() === 'en') ? 'en' : 'ru';\n\n// Определяем тексты\nconst TEXT = {\n ru: {\n title: 'Выберите пакет кредитов:',\n pack: (c) => `${c} Кредитов`,\n },\n en: {\n title: 'Choose a credits pack:',\n pack: (c) => `${c} credits`,\n },\n};\n\nconst T = TEXT[lang] || TEXT.ru;\n\n// Описываем пакеты\nconst packs = [\n { credits: 200 },\n { credits: 500 },\n { credits: 1000 },\n];\n\n// Строим inline_keyboard: по одному пакету в строке\nconst inline_keyboard = packs.map(p => [{\n text: T.pack(p.credits),\n callback_data: `credits:${p.credits}`, // ВАЖНО: формат для Switch_inline_act\n}]);\n\nreturn [{\n json: {\n chatId,\n lang,\n text: T.title,\n reply_markup: {\n inline_keyboard,\n },\n },\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2032,
640
],
"id": "88c3a5a1-7380-49b0-8010-a59a26ce0c95",
"name": "build_credits_packs"
},
{
"parameters": {
"method": "POST",
"url": "https://api.telegram.org/bot8495842221:AAHBriXvzudyazB2f8z3ilGQ8A5ESdjjtl8/sendMessage",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "chat_id",
"value": "={{ $json.chatId }}"
},
{
"name": "text",
"value": "={{ $json.text }}"
},
{
"name": "parse_mode",
"value": "HTML"
},
{
"name": "reply_markup",
"value": "={{ $json.reply_markup }}"
},
{
"name": "disable_notification",
"value": "=true"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2336,
640
],
"id": "80906b35-746c-4a76-ab24-0f859beba4f7",
"name": "reply_markup1",
"retryOnFail": true
},
{
"parameters": {
"method": "POST",
"url": "https://api.telegram.org/bot8495842221:AAHBriXvzudyazB2f8z3ilGQ8A5ESdjjtl8/createInvoiceLink",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.invoicePayload }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-240,
416
],
"id": "96869bd8-cd3e-4b46-8d01-38d35024397a",
"name": "createInvoiceLink_credits"
},
{
"parameters": {
"chatId": "={{ $('build_credits_invoice').item.json.chatId }}",
"text": "={{ $('build_credits_invoice').item.json.ui.caption }}",
"replyMarkup": "inlineKeyboard",
"inlineKeyboard": {
"rows": [
{
"row": {
"buttons": [
{
"text": "={{ $('build_credits_invoice').item.json.ui.btn_stars }}",
"additionalFields": {
"url": "={{ $json.result }}"
}
}
]
}
},
{
"row": {
"buttons": [
{
"text": "={{ $('build_credits_invoice').item.json.ui.btn_card }}",
"additionalFields": {
"url": "={{ $('create_payment_yookassa_credits').item.json.confirmation.confirmation_url }}"
}
}
]
}
}
]
},
"additionalFields": {
"appendAttribution": false,
"parse_mode": "HTML"
}
},
"id": "f9ffaf79-5074-4352-aa5e-d13a34d3b6dd",
"name": "send_mes_payment1",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
192,
512
],
"webhookId": "245ca41a-c6c4-4a94-bb1e-a918af8a165b",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $json.chatId }}",
"messageId": "={{ $json.keyboardMsgId }}"
},
"id": "50b3bb65-e6c0-46d4-b21e-fe33fae4335c",
"name": "delMes_keyboardMsg2",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-464,
832
],
"webhookId": "4dbc69a5-2493-4aac-baca-e4197736aca4",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"jsCode": "// n8n Code — payment_store_prompt\n// Храним последнее \"оплатное\" сообщение с инлайнами в staticData,\n// чтобы потом удалить его после успешной оплаты.\n\nconst sd = $getWorkflowStaticData('global');\nsd.paymentPrompt = sd.paymentPrompt || {}; // { [chatId]: message_id }\n\n// Берём первый входящий item\nconst item = $input.first()?.json || $json || {};\nconst result = item.result || {}; // ответ Telegram sendMessage\nconst chat = result.chat || item.chat || {};\n\nconst chatId = String(\n chat.id ||\n item.chatId ||\n item.db_event?.chat_id ||\n ''\n);\n\nconst messageId = result.message_id;\n\nif (chatId && messageId) {\n sd.paymentPrompt[chatId] = messageId;\n}\n\nreturn [{\n json: {\n chatId,\n messageId,\n stored: !!(chatId && messageId),\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
384,
512
],
"id": "39b6e6e3-f60f-475b-b8fb-3f7e33c15f98",
"name": "payment_store_prompt"
},
{
"parameters": {
"jsCode": "// n8n Code — build_credits_invoice\n// Вход: chatId, lang, pack_credits (200/500/1000)\n// Выход: данные для Telegram Stars + данные для YooKassa (карта)\n\nconst sd = $getWorkflowStaticData('global');\nsd.userLang = sd.userLang || {};\n\nconst chatId = String($json.chatId || '');\nlet lang = (typeof $json.lang === 'string' && $json.lang.toLowerCase() === 'en') ? 'en' : 'ru';\nif (!lang && chatId && sd.userLang[chatId]) lang = sd.userLang[chatId];\n\n// выбранный пак\nconst credits = Number($json.pack_credits || 0);\nif (!Number.isFinite(credits) || credits <= 0) {\n throw new Error('Invalid credits pack selected');\n}\n\n// Прайсинг:\n// amount_stars — цена в Stars (XTR для Telegram)\n// amount_rub — цена в ₽ (для ЮKassa / карты)\nconst pricing = {\n 200: { amount_stars: 159, amount_rub: 199 },\n 500: { amount_stars: 389, amount_rub: 469 },\n 1000:{ amount_stars: 699, amount_rub: 899 },\n};\n\nconst baseCfg = pricing[credits] || pricing[200];\nconst amount_stars = Number(baseCfg.amount_stars) || 0;\nconst amount_rub = Number(baseCfg.amount_rub) || 0;\n\nif (!amount_stars || !amount_rub) {\n throw new Error('Pricing not configured for this credits pack');\n}\n\n// Telegram Stars: минимальные единицы (как «копейки»)\n// Сейчас 1 Star = 1 единица для API (если что — потом поправим unit)\nconst unit = 1;\nconst total_units = amount_stars * unit;\n\n// Локализация текста\nconst T = {\n ru: {\n title: `Пакет ${credits} Кредитов`,\n description: `Покупка ${credits} Кредитов для аккаунта.`,\n // Показываем и Stars, и ₽\n pay_caption:\n `Пакет: ${credits} Кредитов\\n` +\n `Цена звёздами: ${amount_stars} Stars\\n` +\n `Цена по карте: ${amount_rub} ₽`,\n btn_stars: '⭐ Оплатить звёздами',\n btn_card: '💳 Оплатить картой',\n // Для ЮKassa\n yk_description: `Пакет ${credits} кредитов, оплата картой.`,\n },\n en: {\n title: `${credits} Credits pack`,\n description: `Purchase ${credits} credits for your account.`,\n pay_caption:\n `Pack: ${credits} credits\\n` +\n `Price by Stars: ${amount_stars} Stars\\n` +\n `Price by card: ${amount_rub} RUB`,\n btn_stars: '⭐ Pay with Stars',\n btn_card: '💳 Pay by card',\n yk_description: `Pack of ${credits} credits, card payment.`,\n },\n}[lang] || {\n title: `Пакет ${credits} Кредитов`,\n description: `Покупка ${credits} Кредитов для аккаунта.`,\n pay_caption:\n `Пакет: ${credits} Кредитов\\n` +\n `Цена звёздами: ${amount_stars} Stars\\n` +\n `Цена по карте: ${amount_rub} ₽`,\n btn_stars: '⭐ Оплатить звёздами',\n btn_card: '💳 Оплатить картой',\n yk_description: `Пакет ${credits} кредитов, оплата картой.`,\n};\n\n// payload под текущий payment.parse_success:\n// \"credits:200\", \"credits:500\", \"credits:1000\"\nconst payload = `credits:${credits}`;\n\n// === Telegram Stars invoice payload ===\nconst invoicePayload = {\n chat_id: Number(chatId),\n title: T.title,\n description: T.description,\n payload,\n currency: 'XTR',\n prices: [\n { label: T.title, amount: total_units }\n ]\n};\n\n// Пока заглушка под Робокассу (можно будет заменить на реальную ссылку)\nconst robokassa_url = 'https://example.com/robokassa/credits';\n\n// === YooKassa: подготовка данных ===\n\n// product_code — удобно использовать как \"credits_200\" / \"credits_500\" / \"credits_1000\"\nconst yk_product_code = `credits_${credits}`;\n\n// ЮKassa ждёт amount.value строкой с двумя знаками после запятой\nconst yk_amount_value = amount_rub.toFixed(2);\nconst yk_currency = 'RUB';\n\n// Описание платежа для ЮKassa\nconst yk_description = T.yk_description;\n\n// metadata — то, что вернётся в вебхуке (payment.succeeded)\nconst yk_metadata = {\n tg_user_id: chatId,\n credits,\n product_code: yk_product_code,\n source: 'telegram-bot',\n lang,\n};\n\nreturn [{\n json: {\n chatId,\n lang,\n credits,\n\n // Stars\n price_stars: amount_stars,\n\n // Рубли (для карты / ЮKassa)\n price_rub: amount_rub,\n\n // Telegram Stars invoice\n invoicePayload,\n robokassa_url,\n\n // UI для текущих сообщений\n ui: {\n caption: T.pay_caption,\n btn_stars: T.btn_stars,\n btn_card: T.btn_card,\n },\n\n // YooKassa fields\n yk_product_code,\n yk_amount_value,\n yk_currency,\n yk_description,\n yk_metadata,\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-464,
512
],
"id": "ceb2314c-fd38-45fe-9ae5-f4a989e2a37f",
"name": "build_credits_invoice"
},
{
"parameters": {
"jsCode": "// n8n Code — payment_pick_prompt_to_delete\n// Забираем сохранённый message_id инлайна оплаты и готовим данные для deleteMessage.\n\nconst sd = $getWorkflowStaticData('global');\nsd.paymentPrompt = sd.paymentPrompt || {}; // { [chatId]: message_id }\n\n// В этом месте у нас JSON от payment.parse_success / apply_payment\nconst src = $json || {};\nconst chatId = String(\n src.chatId ||\n src.db_event?.chat_id ||\n ''\n);\n\nlet delete_message_id = null;\n\nif (chatId && sd.paymentPrompt[chatId]) {\n delete_message_id = sd.paymentPrompt[chatId];\n // одноразово: после удаления не храним\n delete sd.paymentPrompt[chatId];\n}\n\nreturn [{\n json: {\n ...src,\n delete_chat_id: chatId || null,\n delete_message_id: delete_message_id,\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-640,
-864
],
"id": "94f16d79-e687-470c-8dae-893f6db39790",
"name": "payment_pick_prompt_to_delete"
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $json.chatId }}",
"messageId": "={{ $json.delete_message_id }}"
},
"id": "7efa892e-154f-43c1-b314-27d7710671df",
"name": "delMes3",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-384,
-864
],
"webhookId": "cb8798c8-d243-4bc9-bf94-f81f77c99c14",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://bwbsclwdkighhzlyiman.supabase.co/rest/v1/rpc/wallet_grant_welcome_once",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": " application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"p_tg_user_id\": \"{{ $('ensure_user').item.json.tg_user_id }}\",\n \"p_tg_username\": \"{{ $('ensure_user').item.json.tg_username }}\",\n \"p_amount\": 10,\n \"p_lang\": \"{{ $json.lang || 'ru' }}\"\n}\n",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
816,
1248
],
"id": "3e1098f2-702c-4209-84a2-d91ba85747c9",
"name": "welcome_once",
"retryOnFail": true,
"credentials": {
"supabaseApi": {
"id": "jGgpXKPYHiL193Rz",
"name": "Supabase account"
}
}
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.is_start }}",
"rightValue": "gpt",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.is_start }}",
"rightValue": "=flux",
"operator": {
"type": "boolean",
"operation": "false",
"singleValue": true
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
112,
2016
],
"id": "d5e4be4b-38b1-49ba-8644-30002f28a152",
"name": "switch_start"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.welcome_applied }}",
"rightValue": "gpt",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.welcome_applied }}",
"rightValue": "=flux",
"operator": {
"type": "boolean",
"operation": "false",
"singleValue": true
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
1024,
1248
],
"id": "74d71777-4e15-4326-b5be-3ea691b0b6a6",
"name": "switch_welcome"
},
{
"parameters": {
"chatId": "={{ $('ensure_user').item.json.tg_user_id }}",
"text": "={{ $('ensure_user').item.json.lang === 'en'\n ? '🎁 We have credited 10 welcome credits to your account.'\n : '🎁 Мы начислили вам 10 приветственных Кредитов.'\n}}",
"additionalFields": {
"appendAttribution": false
}
},
"id": "765d90e2-2da4-4a68-9f89-767f2e1ea456",
"name": "send_wel_cred",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1232,
1248
],
"webhookId": "a2aae37a-1bcb-481d-adcf-d43a7b4b27a6",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"chatId": "={{ $('switch_start').item.json.chatId }}",
"text": "={{ $('switch_start').item.json.lang === 'en'\n ? '🫡 Welcome to GENI AI!'\n : '🫡 Приветствую в GENI AI!'\n}}\n{{ $json.lang === 'en' ? '🌐 Choose your language:' : '🌐 Выберите язык:' }}",
"replyMarkup": "inlineKeyboard",
"inlineKeyboard": {
"rows": [
{
"row": {
"buttons": [
{
"text": "Русский",
"additionalFields": {
"callback_data": "setlang:ru"
}
}
]
}
},
{
"row": {
"buttons": [
{
"text": "English ",
"additionalFields": {
"callback_data": "setlang:en"
}
}
]
}
}
]
},
"additionalFields": {
"appendAttribution": false
}
},
"id": "70f05f2c-cfc0-48e8-8945-9d67c3acdd03",
"name": "send_start_mes",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
816,
1040
],
"webhookId": "1969a35d-d09b-43a2-9df5-891be378c6cf",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"jsCode": "// lang_to_home_after_set\n// Генерируем \"фейковое\" текстовое сообщение `/menu`,\n// чтобы прогнать его через обычный роутинг (Switch_message_type → ensure_user → menu → reply_markup).\n\nconst chatId = String($json.chatId || $input.first().json.result.chat.id);\nconst lang = typeof $('SB_set_user_lang').first().json.lang === 'string' ? $('SB_set_user_lang').first().json.lang.toLowerCase() : null;\n\nif (!chatId) {\n // На всякий случай, если вдруг что-то пошло не так — просто ничего не делаем\n return [];\n}\n\nconst now = Math.floor(Date.now() / 1000);\n\nreturn [{\n json: {\n // Эмулируем update.message, как будто Telegram прислал /menu\n message: {\n message_id: 0, // фиктивный id — он нужен только для совместимости\n from: {\n id: Number(chatId) || chatId,\n is_bot: false,\n language_code: lang || 'ru',\n },\n chat: {\n id: Number(chatId) || chatId,\n type: 'private',\n },\n date: now,\n text: '/menu',\n entities: [\n {\n offset: 0,\n length: 5,\n type: 'bot_command',\n },\n ],\n },\n // Дополнительно можно пробросить lang, но ensure_user уже вернёт актуальный\n lang: lang || undefined,\n },\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
160,
32
],
"id": "5b35d56b-e756-40f6-b8fa-175fea97c464",
"name": "lang_to_home_after_set"
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $json.result.chat.id }}",
"messageId": "={{ $('Switch1').item.json.deleteMessageId }}"
},
"id": "bbfccd08-7ba1-426e-ab8f-331276322a8c",
"name": "delMes4",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2224,
1264
],
"webhookId": "5af3ec5f-9501-4891-bb28-8d52e321d7cc",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "// lang_to_home_after_set\n// Генерируем \"фейковое\" текстовое сообщение `/menu`,\n// чтобы прогнать его через обычный роутинг (Switch_message_type → ensure_user → menu → \nconst chatId = String($('Switch1').first().json.chatId);\nconst lang = typeof $('Switch1').first().json.lang === 'string' ? $('Switch1').first().json.lang.toLowerCase() : null;\n\nif (!chatId) {\n // На всякий случай, если вдруг что-то пошло не так — просто ничего не делаем\n return [];\n}\n\nconst now = Math.floor(Date.now() / 1000);\n\nreturn [{\n json: {\n // Эмулируем update.message, как будто Telegram прислал /menu\n message: {\n message_id: 0, // фиктивный id — он нужен только для совместимости\n from: {\n id: Number(chatId) || chatId,\n is_bot: false,\n language_code: lang || 'ru',\n },\n chat: {\n id: Number(chatId) || chatId,\n type: 'private',\n },\n date: now,\n text: '/menu',\n entities: [\n {\n offset: 0,\n length: 5,\n type: 'bot_command',\n },\n ],\n },\n // Дополнительно можно пробросить lang, но ensure_user уже вернёт актуальный\n lang: lang || undefined,\n },\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2464,
1264
],
"id": "98ba3ef7-444e-46e9-b1f2-73d9d293c3bf",
"name": "lang_to_home_after_set1"
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.lang === 'en'\n ? '📘 How to use Geni AI\\n\\n' +\n '1. Open the menu and choose 🖼 Image → ⚡️ FLUX.\\n' +\n '2. Send a text prompt (and optionally a reference photo).\\n' +\n '3. The bot will generate images and spend credits for each request.\\n\\n' +\n '💰 Balance & credits:\\n' +\n '- Use 👤 Account → 💰 Balance to see your credits.\\n' +\n '- Use 👤 Account → ⚡️ Buy credits to top up.\\n\\n' +\n 'If you run out of credits, the bot will show how to buy more.\\n' +\n 'If you need help text to me @GRIGORIY_VOYAKIN.'\n : '📘 Как пользоваться Geni AI\\n\\n' +\n '1. Откройте меню и выберите 🖼 Image → ⚡️ FLUX.\\n' +\n '2. Отправьте текстовый промпт (можно добавить референс-фото).\\n' +\n '3. Бот сгенерирует изображения и спишет Кредиты за запрос.\\n\\n' +\n '💰 Баланс и Кредиты:\\n' +\n '- В разделе 👤 Аккаунт → 💰 Баланс можно посмотреть остаток.\\n' +\n '- В 👤 Аккаунт → ⚡️ Купить кредиты пополнить счёт.\\n\\n' +\n 'Если Кредитов не хватит, бот покажет, как их докупить.\\n' +\n 'Если нужна помощь, напишите мне @GRIGORIY_VOYAKIN.'\n}}",
"additionalFields": {
"appendAttribution": false,
"parse_mode": "HTML"
}
},
"id": "80a83edf-7382-4d9d-8c31-f491521d772b",
"name": "Instruct_mes",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2016,
1264
],
"webhookId": "c063a8aa-5e3d-4f7f-9969-213d4a97cb49",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $('switch_reply_menu').item.json.homeInlineShow }}",
"rightValue": "gpt",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $('switch_reply_menu').item.json.homeInlineShow }}",
"rightValue": "=flux",
"operator": {
"type": "boolean",
"operation": "false",
"singleValue": true
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
624,
1568
],
"id": "450f61d7-17f9-4f05-be64-cadd2feaecf7",
"name": "switch_home_inline",
"disabled": true
},
{
"parameters": {
"method": "POST",
"url": "https://api.telegram.org/bot8495842221:AAHBriXvzudyazB2f8z3ilGQ8A5ESdjjtl8/sendMessage",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "chat_id",
"value": "={{ $('switch_reply_menu').item.json.chatId }}"
},
{
"name": "text",
"value": "={{ $('switch_reply_menu').item.json.homeInlineText }}"
},
{
"name": "parse_mode",
"value": "HTML"
},
{
"name": "reply_markup",
"value": "={{ $('switch_reply_menu').item.json.homeInlineMarkup }}"
},
{
"name": "disable_notification",
"value": "={{ $('switch_reply_menu').item.json.homeInlineShow }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
816,
1568
],
"id": "8133c5ce-d354-46fd-b97f-ee56e2787aa7",
"name": "send_home_inline",
"retryOnFail": true,
"disabled": true
},
{
"parameters": {
"mode": "combine",
"combineBy": "combineByPosition",
"options": {}
},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
-16,
512
],
"id": "8d564289-84e6-4048-9815-87c5901d4763",
"name": "Merge"
},
{
"parameters": {
"method": "POST",
"url": "=https://bwbsclwdkighhzlyiman.supabase.co/rest/v1/payment_messages",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": " application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"provider\": \"yookassa\",\n \"charge_id\": \"{{ $('create_payment_yookassa_credits').item.json.id }}\",\n \"chat_id\": {{ $json.result.chat.id }},\n \"message_id\": {{ $json.result.message_id }}\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
384,
720
],
"id": "d8aa30d1-3f09-4ebe-8189-acff9d636cbe",
"name": "store_payment_message",
"retryOnFail": true,
"credentials": {
"supabaseApi": {
"id": "jGgpXKPYHiL193Rz",
"name": "Supabase account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "https://api.yookassa.ru/v3/payments",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Idempotence-Key",
"value": "={{ $json.chatId + ':' + $json.credits + ':' + Date.now() }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"amount\": {\n \"value\": \"{{ $json.price_rub }}\",\n \"currency\": \"RUB\"\n },\n \"capture\": true,\n \"confirmation\": {\n \"type\": \"redirect\",\n \"return_url\": \"https://t.me/Geni_AI_bot\"\n },\n \"description\": \"{{ `Пакет ${$json.credits} кредитов. TG: ${$json.chatId}` }}\",\n \"metadata\": {\n \"tg_chat_id\": \"{{ $json.chatId }}\",\n \"product\": \"credits\",\n \"credits\": \"{{ $json.credits }}\",\n \"lang\": \"{{ $json.yk_metadata.lang }}\"\n },\n \"receipt\": {\n \"customer\": {\n \"email\": \"grigoriyvoyakinwork@gmail.com\"\n },\n \"items\": [\n {\n \"description\": \"{{ `Пакет ${$json.credits} кредитов` }}\",\n \"quantity\": 1,\n \"amount\": {\n \"value\": \"{{ $json.price_rub }}\",\n \"currency\": \"RUB\"\n },\n \"vat_code\": 1\n }\n ]\n }\n}\n",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-240,
640
],
"id": "200b1c87-9d9b-454c-97a4-043c941bfd85",
"name": "create_payment_yookassa_credits",
"credentials": {
"httpBasicAuth": {
"id": "eoZ95FIg2ZXlecAm",
"name": "yookassa_prod"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://bwbsclwdkighhzlyiman.supabase.co/rest/v1/rpc/get_user_id_and_balance",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": " application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"p_tg_user_id\": \"{{ $json.chatId }}\"\n}\n",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2032,
48
],
"id": "f30d1b6f-58db-44e1-bb7f-1ea561dc249b",
"name": "id_and_balance",
"retryOnFail": true,
"credentials": {
"supabaseApi": {
"id": "jGgpXKPYHiL193Rz",
"name": "Supabase account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://bwbsclwdkighhzlyiman.supabase.co/rest/v1/rpc/ensure_user",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": " application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"p_tg_user_id\": \"{{ $json.chatId }}\",\n \"p_tg_username\": \"{{ $('Telegram Trigger1').item.json.message?.from?.username || '' }}\"\n}\n",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
624,
1184
],
"id": "ff277c5c-10bc-4522-a6f7-63b675ba9e58",
"name": "ensure_user",
"retryOnFail": true,
"credentials": {
"supabaseApi": {
"id": "jGgpXKPYHiL193Rz",
"name": "Supabase account"
}
}
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.action }}",
"rightValue": "STORED",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.action }}",
"rightValue": "=GENERATE",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "9cff2c53-bb3e-49bf-9cfe-53963eac8978",
"leftValue": "={{ $json.action }}",
"rightValue": "CLEARED",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "39ccebaf-cc95-4d0b-954f-71fab6178691",
"leftValue": "={{ $json.action }}",
"rightValue": "NOOP ",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "a9f7ce9b-cd18-4138-83f1-8cb70c4643ed",
"leftValue": "={{ $json.action }}",
"rightValue": "REMIND",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
2032,
2224
],
"id": "d13fd2f7-d37a-42d1-b4bb-b2748182316c",
"name": "Switch2"
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.lang === 'en'\n ? 'Send a photo or a prompt for generation.'\n : 'Пришлите фото или промпт для генерации.'\n}}",
"additionalFields": {
"appendAttribution": false
}
},
"id": "06fdf684-8a54-40e4-b5b3-d2c2e1d2af36",
"name": "Telegram18",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2304,
2704
],
"webhookId": "192bb390-c119-4d8f-b8cc-bbfe51bc0372",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.lang === 'en'\n ? 'All uploaded photos have been cleared.'\n : 'Все загруженные фото сброшены.'\n}}",
"additionalFields": {
"appendAttribution": false
}
},
"id": "deea1c46-34cc-4516-8998-197702f9353f",
"name": "Telegram19",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2304,
2512
],
"webhookId": "3cea3fd1-dd4f-4d78-9b0e-200130d147c1",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.lang === 'en'\n ? `${$json.reply_text}\\n⚡ Generation cost: ${$json.price} credits.\\nTap a button to delete.`\n : `${$json.reply_text}\\n⚡Стоимость генерации: ${$json.price} кредитов.\\nНажмите кнопку, чтобы удалить.`\n}}",
"replyMarkup": "inlineKeyboard",
"inlineKeyboard": {
"rows": [
{
"row": {
"buttons": [
{
"text": "={{ $json.lang === 'en'\n ? '🗑 Delete this photo'\n : '🗑 Удалить это фото'\n}}",
"additionalFields": {
"callback_data": "del:${$json.photo_message_id}"
}
}
]
}
},
{
"row": {
"buttons": [
{
"text": "={{ $json.lang === 'en'\n ? '🧺 Clear all'\n : '🧺 Очистить все'\n}}",
"additionalFields": {
"callback_data": "del_all"
}
}
]
}
}
]
},
"additionalFields": {
"appendAttribution": false,
"reply_to_message_id": "={{ $json.reply_to_message_id }}"
}
},
"id": "72a12cdd-2b6e-41a4-967f-a1799ccbdfe3",
"name": "Telegram20",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2480,
2080
],
"webhookId": "6969c97f-2c91-44e1-ae46-426d11c99897",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"jsCode": "const sd = $getWorkflowStaticData('global');\nsd.photoIndex = sd.photoIndex || {}; // { [chatId]: { [messageId]: file_id } }\n\nconst chatId = String($json.chatId || '');\nconst msgId = Number($json.photo_message_id ?? $json.deleteMessageId ?? 0);\nconst fileId = $json.file_id || $json.photo_file_id || $json.primary_file?.file_id || null;\n\n\nif (chatId && msgId && fileId) {\n sd.photoIndex[chatId] = sd.photoIndex[chatId] || {};\n sd.photoIndex[chatId][msgId] = fileId;\n}\n\nreturn [{ json: $json }];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2304,
2080
],
"id": "6a4b738a-17f0-40fb-b913-75b0cc157b72",
"name": "Code15"
},
{
"parameters": {
"jsCode": "const sd = $getWorkflowStaticData('global');\nsd.replyIndex = sd.replyIndex || {}; // { [chatId]: number[] }\n\nconst chatId = String($json.chatId || $json.result?.chat?.id || '');\nconst sentId =\n Number($json.result?.message_id) ||\n Number($json.result?.message?.message_id) ||\n Number($json.message_id) || 0;\n\nif (chatId && sentId) {\n sd.replyIndex[chatId] = sd.replyIndex[chatId] || [];\n if (!sd.replyIndex[chatId].includes(sentId)) sd.replyIndex[chatId].push(sentId);\n}\n\nreturn [{ json: $json }];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2656,
2080
],
"id": "a0a675a4-7296-4e10-b9fe-cdbba0206956",
"name": "Code23"
},
{
"parameters": {
"jsCode": "// FLUX controller (t2i/i2i) — unified cache only\n// Outputs: action, chatId, reply_text, settings_text, reply_to_message_id,\n// prompt, photo_file_id, ar, useRef, ref, refInterp, seed, quality, lang, balance\n\nconst sd = $getWorkflowStaticData('global');\nsd.flux = sd.flux || {}; // { [chatId]: { ar, ref, seed, quality, useRef } }\nsd.photoBucket = sd.photoBucket || {}; // { [chatId]: { files:[{file_id, message_id, ...}], savedAt } }\nsd.userLang = sd.userLang || {};\n\nconst msg = $json.message || {};\nconst chatId = String($json.chatId || msg.chat?.id || $json.chat?.id || '');\nconst lang = (typeof $json.lang === 'string' && /^(ru|en)$/i.test($json.lang))\n ? $json.lang.toLowerCase()\n : (sd.userLang[chatId] || (/^en/i.test(String(msg.from?.language_code||'')) ? 'en' : 'ru'));\nsd.userLang[chatId] = lang;\n\nconst balance = Number.isFinite(Number($json.balance)) ? Number($json.balance) : null;\nconst emit = (payload) => [{ json: { ...payload, lang, balance } }];\n\nconst promptNorm = typeof $json.prompt === 'string' ? $json.prompt : '';\nconst tgText = typeof msg.text === 'string' ? msg.text : (typeof msg.caption === 'string' ? msg.caption : '');\nconst prompt = (promptNorm || tgText || '').trim();\nconst hasText = prompt.length > 0;\nconst reply_to_message_id = (msg.message_id ?? $json.deleteMessageId ?? null);\n\n// I18N\nconst I18N = {\n ru: {\n stored: (n) => `📥 Фото сохранено (${n}). Отправьте текст — запущу i2i.`,\n remind: `Пришлите текст — запущу t2i, или фото затем текст — запущу i2i.`,\n noop: `Отправьте текст для запуска.`,\n settings: (s) => [\n `🧩 FLUX (auto t2i/i2i)`,\n `📐 AR: ${s.ar} · ${s.dimText}`,\n `🎛 Ref: ${s.useRef ? s.ref.toFixed(2) : 'off'} (interp: ${s.refInterp.toFixed(2)})`,\n `🌱 Seed: ${s.seed === 0 ? 'random' : s.seed}`,\n `⚙️ Quality: ${s.quality === 0 ? 'fast' : 'high'}`\n ].join('\\n'),\n },\n en: {\n stored: (n) => `📥 Photo saved (${n}). Send text to run i2i.`,\n remind: `Send text to run t2i, or photo then text to run i2i.`,\n noop: `Send a text prompt to start.`,\n settings: (s) => [\n `🧩 FLUX (auto t2i/i2i)`,\n `📐 AR: ${s.ar} · ${s.dimText}`,\n `🎛 Ref: ${s.useRef ? s.ref.toFixed(2) : 'off'} (interp: ${s.refInterp.toFixed(2)})`,\n `🌱 Seed: ${s.seed === 0 ? 'random' : s.seed}`,\n `⚙️ Quality: ${s.quality === 0 ? 'fast' : 'high'}`\n ].join('\\n'),\n }\n};\nconst T = I18N[lang] || I18N.ru;\n\n// Flux settings\nconst defaults = { ar: '1:1', ref: 0.75, seed: 0, quality: 1, useRef: false };\nconst stored = (sd.flux[chatId] && typeof sd.flux[chatId] === 'object') ? sd.flux[chatId] : defaults;\n\nlet seedRaw = Number(stored.seed ?? 0);\nif (!Number.isFinite(seedRaw)) seedRaw = 0;\nconst seed = (seedRaw === 0) ? Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) : seedRaw;\n\nlet ref = Number(stored.ref ?? defaults.ref);\nif (!Number.isFinite(ref)) ref = defaults.ref;\nref = Math.max(0, Math.min(1.2, ref));\nconst refInterp = (() => {\n if (!Number.isFinite(ref) || ref <= 0) return 0;\n const t = Math.min(Math.max(ref / 1.2, 0), 1);\n return 5 + (1 - 5) * t;\n})();\n\nlet qRaw = Number(stored.quality ?? defaults.quality);\nconst quality = Number.isFinite(qRaw) ? qRaw : 1;\n\nfunction arToDims(ar) {\n const [w, h] = String(ar || '1:1').split(':').map(n => parseInt(n,10) || 1);\n const base = 1024;\n const W = (w >= h) ? base : Math.round(base * w / h);\n const H = (w >= h) ? Math.round(base * h / w) : base;\n return `${W}×${H}`;\n}\nconst ar = (typeof stored.ar === 'string' && /^\\d+:\\d+$/.test(stored.ar)) ? stored.ar : defaults.ar;\nconst dimText = arToDims(ar);\n\n// detect incoming image\nlet incoming = null;\nif ($json.photo_file_id) {\n incoming = {\n file_id: $json.photo_file_id,\n kind: $json.file_kind || 'photo',\n file_name: $json.file_name || null,\n mime_type: $json.mime_type || null,\n message_id: (msg.message_id ?? $json.deleteMessageId ?? null),\n savedAt: new Date().toISOString()\n };\n}\nif (!incoming && msg.document?.file_id) {\n const mt = String(msg.document.mime_type || '').toLowerCase();\n const name = String(msg.document.file_name || '');\n const looksImage = mt.startsWith('image/') || /\\.(png|jpe?g|webp|bmp|gif|tiff?)$/i.test(name);\n if (looksImage) {\n incoming = {\n file_id: msg.document.file_id,\n kind: 'document',\n file_name: name || null,\n mime_type: msg.document.mime_type || null,\n message_id: (msg.message_id ?? $json.deleteMessageId ?? null),\n savedAt: new Date().toISOString()\n };\n }\n}\nif (!incoming && Array.isArray(msg.photo) && msg.photo.length) {\n const best = msg.photo[msg.photo.length - 1];\n incoming = {\n file_id: best.file_id,\n kind: 'photo',\n file_name: null,\n mime_type: null,\n message_id: (msg.message_id ?? $json.deleteMessageId ?? null),\n savedAt: new Date().toISOString()\n };\n}\n\n// helpers\nfunction ensureBucket() {\n return sd.photoBucket[chatId] ?? { files: [], savedAt: new Date().toISOString() };\n}\nfunction pushLimited(b, f, limit=4) {\n const exists = (b.files || []).some(x => x.file_id === f.file_id);\n if (!exists) {\n (b.files = b.files || []).push(f);\n if (b.files.length > limit) b.files = b.files.slice(-limit);\n }\n return b;\n}\n\n// guards\nif (!chatId) {\n const settings_text = T.settings({ ar, dimText, useRef:false, ref:0, refInterp:0, seed, quality });\n return emit({\n action: 'REMIND', chatId: '',\n reply_text: 'no chat', settings_text, reply_to_message_id: reply_to_message_id ?? null,\n prompt:'', photo_file_id:null, ar, useRef:false, ref:0, refInterp:0, seed, quality\n });\n}\n\n// /clear → очистка бакета\nif (/^\\/clear\\b/i.test(prompt)) {\n if (sd.photoBucket[chatId]?.files) sd.photoBucket[chatId].files = [];\n const settings_text = T.settings({ ar, dimText, useRef:false, ref, refInterp:0, seed, quality });\n return emit({\n action: 'CLEARED',\n chatId,\n reply_text: lang==='en' ? '🗑️ Reference bucket cleared.' : '🗑️ Копилка референсов очищена.',\n settings_text,\n reply_to_message_id,\n prompt: '',\n photo_file_id: null,\n ar, useRef:false, ref:0, refInterp:0, seed, quality\n });\n}\n\n// flow\nconst bucket = sd.photoBucket[chatId] || { files: [] };\nconst files = bucket.files || [];\nconst hasStoredPhoto = files.length > 0;\n\n// 1) Фото без текста → складируем\nif (incoming && !hasText) {\n const b = pushLimited(ensureBucket(), incoming, 4);\n sd.photoBucket[chatId] = b;\n const settings_text = T.settings({ ar, dimText, useRef:true, ref, refInterp, seed, quality });\n return emit({\n action: 'STORED',\n chatId,\n reply_text: T.stored(b.files.length),\n settings_text,\n reply_to_message_id,\n prompt: '',\n photo_file_id: null,\n ar, useRef:true, ref, refInterp, seed, quality\n });\n}\n\n// 2) t2i\nif (hasText && !hasStoredPhoto && !incoming) {\n const settings_text = T.settings({ ar, dimText, useRef:false, ref:0, refInterp:0, seed, quality });\n return emit({\n action: 'GENERATE',\n chatId,\n reply_text: '',\n settings_text,\n reply_to_message_id,\n prompt,\n photo_file_id: null,\n ar, useRef:false, ref:0, refInterp:0, seed, quality\n });\n}\n\n// 3) i2i\nif (hasText && (hasStoredPhoto || incoming)) {\n const all = hasStoredPhoto ? files.slice() : [];\n if (incoming) all.push(incoming);\n const primary = all[all.length - 1];\n\n const settings_text = T.settings({ ar, dimText, useRef:true, ref, refInterp, seed, quality });\n sd.photoBucket[chatId] = { files: all.slice(-4), savedAt: (bucket.savedAt || new Date().toISOString()) };\n\n return emit({\n action: 'GENERATE',\n chatId,\n reply_text: '',\n settings_text,\n reply_to_message_id,\n prompt,\n photo_file_id: primary?.file_id || null,\n ar, useRef:true, ref, refInterp, seed, quality\n });\n}\n\n// 4) подсказки/фолбек\nif (!hasText && !incoming && !hasStoredPhoto) {\n const settings_text = T.settings({ ar, dimText, useRef:false, ref:0, refInterp:0, seed, quality });\n return emit({\n action: 'REMIND',\n chatId,\n reply_text: T.remind,\n settings_text,\n reply_to_message_id,\n prompt: '',\n photo_file_id: null,\n ar, useRef:false, ref:0, refInterp:0, seed, quality\n });\n}\n\nconst settings_text = T.settings({ ar, dimText, useRef:true, ref, refInterp, seed, quality });\nreturn emit({\n action: 'NOOP',\n chatId,\n reply_text: T.noop,\n settings_text,\n reply_to_message_id,\n prompt: '',\n photo_file_id: null,\n ar, useRef:true, ref, refInterp, seed, quality\n});\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1664,
2272
],
"id": "3e318ac2-354b-40dd-a744-cc1318b54142",
"name": "Flux settings"
},
{
"parameters": {
"jsCode": "// n8n Code node — Clear caches after generation (UNIFIED FORMAT)\n// Берёт chatId из ответа Telegram sendPhoto / sendDocument:\n// { ok: true, result: { chat: { id: ... }, ... } }\n//\n// ЧТО ДЕЛАЕТ:\n// - sd.photoBucket[chatId] (CLEARED)\n// - sd.replyIndex[chatId] (CLEARED)\n// - sd.photoIndex[chatId] (PRUNE по message_id; сама map остаётся)\n// $json.deleted_message_ids — по-прежнему опционален.\n\nconst sd = $getWorkflowStaticData('global');\n\n// Ensure unified caches exist\nsd.photoBucket = sd.photoBucket || {}; // { [chatId]: { files:[{file_id, message_id, ...}], savedAt } }\nsd.photoIndex = sd.photoIndex || {}; // { [chatId]: { [messageId]: file_id } }\nsd.replyIndex = sd.replyIndex || {}; // { [chatId]: number[] }\nsd.poll = sd.poll || {}; // left intact\n\n// ---- Определяем chatId из входящего item (ответ Telegram)\nconst item = $input.first()?.json || $json || {};\n\nconst chatId = $('Switch2').first().json.chatId;\n\n// Если вдруг чата нет — ничего не трогаем, но ok=true, чтобы пайплайн не падал\nif (!chatId) {\n return [{\n json: {\n ok: true,\n chatId: null,\n skipped: 'no_chatId_in_input'\n }\n }];\n}\n\n// BEFORE snapshot\nconst before = {\n photoBucket_files: sd.photoBucket?.[chatId]?.files?.length || 0,\n photoIndex_keys: sd.photoIndex?.[chatId] ? Object.keys(sd.photoIndex[chatId]).length : 0,\n replyIndex_len: Array.isArray(sd.replyIndex?.[chatId]) ? sd.replyIndex[chatId].length : 0,\n};\n\n// Собираем message_id для зачистки в photoIndex:\n// 1) из текущего photoBucket[chatId].files\n// 2) опционально — из $json.deleted_message_ids (если кто-то сверху передал)\nconst toPruneSet = new Set();\n\n// from bucket\nconst bucketFiles = sd.photoBucket?.[chatId]?.files || [];\nfor (const x of bucketFiles) {\n const mid = Number(x?.message_id);\n if (Number.isFinite(mid) && mid > 0) toPruneSet.add(String(mid));\n}\n\n// from payload (optional)\nconst extraIds = Array.isArray($json.deleted_message_ids) ? $json.deleted_message_ids : [];\nfor (const mid of extraIds) {\n const n = Number(mid);\n if (Number.isFinite(n) && n > 0) toPruneSet.add(String(n));\n}\n\n// Clear bucket + replyIndex для этого чата\ndelete sd.photoBucket[chatId];\ndelete sd.replyIndex[chatId];\n\n// Prune только нужные ключи из photoIndex (не дропаем карту целиком)\nlet prunedCount = 0;\nif (sd.photoIndex[chatId] && toPruneSet.size > 0) {\n for (const key of toPruneSet) {\n if (Object.prototype.hasOwnProperty.call(sd.photoIndex[chatId], key)) {\n delete sd.photoIndex[chatId][key];\n prunedCount++;\n }\n }\n // Можно было бы удалить sd.photoIndex[chatId], если она опустела,\n // но оставляем, чтобы не дёргать структуру.\n}\n\n// AFTER snapshot\nconst after = {\n photoBucket_files: sd.photoBucket?.[chatId]?.files?.length || 0,\n photoIndex_keys: sd.photoIndex?.[chatId] ? Object.keys(sd.photoIndex[chatId]).length : 0,\n replyIndex_len: Array.isArray(sd.replyIndex?.[chatId]) ? sd.replyIndex[chatId].length : 0,\n};\n\nreturn [{\n json: {\n ok: true,\n chatId,\n pruned_from_photoIndex: prunedCount,\n before,\n after\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2656,
2304
],
"id": "07cb610e-d636-4d67-bbf6-6648a048af39",
"name": "Clean_memory"
},
{
"parameters": {
"jsCode": "// n8n Code — FLUX: price + batch + aspect (w,h)\n\n// Ожидаем, что в $json уже есть:\n// - ar — строка формата \"16:9\", \"3:4\" и т.п. (дефолт \"1:1\")\n// - quality — 0 | 1 | 2 (дефолт 1)\n// - batch / batch_count — на будущее, сейчас из вебаппа не приходит (дефолт 1)\n\nconst arRaw = $json.ar ?? '1:1';\nlet quality = Number($json.quality);\nif (!Number.isFinite(quality) || quality < 0 || quality > 2) {\n quality = 1; // дефолт Standard\n}\n\n// --- Таблица по качеству ---\n// Можно интерпретировать так: \"стоимость одной картинки в кредитах\"\nconst QUALITY_TABLE = {\n 0: { label: 'light', credits: 1.5 },\n 1: { label: 'standard',credits: 2.0 },\n 2: { label: 'pro', credits: 3.0 },\n};\n\n// --- Таблица по батчу (множитель) ---\n// batch = 1 → множитель 1\n// batch = 2 → 1.75\n// batch = 4 → 3\nlet batchCount = Number($json.batch_count ?? $json.batch ?? 1);\nif (!Number.isFinite(batchCount) || ![1, 2, 4].includes(batchCount)) {\n batchCount = 1;\n}\n\nconst BATCH_TABLE = {\n 1: { multiplier: 1.0 },\n 2: { multiplier: 1.75 },\n 4: { multiplier: 3.0 },\n};\n\n// Берём конфиги (с фолбэком на дефолты)\nconst qCfg = QUALITY_TABLE[quality] ?? QUALITY_TABLE[1];\nconst bCfg = BATCH_TABLE[batchCount] ?? BATCH_TABLE[1];\n\n// Итоговая цена в кредитах\nconst creditsCost = qCfg.credits * bCfg.multiplier;\n\n// --- aspect → w,h ---\n// arRaw, например, \"16:9\"\nconst parts = String(arRaw).split(':');\nconst w = parseInt(parts[0], 10) || 1;\nconst h = parseInt(parts[1], 10) || 1;\n\n// Собираем расширенный json\nreturn [{\n json: {\n ...$json,\n quality,\n batch_count: batchCount,\n price: creditsCost,\n w,\n h,\n quality,\n batch_count: batchCount,\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1840,
2272
],
"id": "6377556e-b9a2-41a8-b92e-2add18236be4",
"name": "flux_price_and_aspect"
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "leS7vcfjDNDlHg0o",
"mode": "list",
"cachedResultUrl": "/workflow/leS7vcfjDNDlHg0o",
"cachedResultName": "Geni_AI_FLUX_v002"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {
"waitForSubWorkflow": true
}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
2304,
2304
],
"name": "Call FLUX",
"id": "1919d1ec-14fd-4573-ad60-9a233c235d90"
},
{
"parameters": {
"jsCode": "// Seedream photo-bucket controller (унификация с Seedance)\n// Авто-режим: если есть фото → i2i; иначе → t2i.\n// Настройки читаем из sd.seedream[chatId] (quality 1k/2k/4k, ratio для t2i).\n\nconst sd = $getWorkflowStaticData('global');\nsd.photoBucket = sd.photoBucket || {}; // { [chatId]: { files:[{file_id,...}], savedAt } }\nsd.seedream = sd.seedream || {}; // { [chatId]: { quality, ratio, dims_t2i, long_edge_i2i, savedAt } }\n\n// ---------- inputs (normalized + telegram) ----------\nconst msg = $json.message || {};\nconst chatId = String($json.chatId || msg.chat?.id || $json.chat?.id || '');\nconst tool = $json.tool || 'seedream';\n\nconst normalizedPrompt = typeof $json.prompt === 'string' ? $json.prompt : '';\nconst tgText = typeof msg.text === 'string' ? msg.text : (typeof msg.caption === 'string' ? msg.caption : '');\nconst prompt = (normalizedPrompt || tgText || '').trim();\nconst hasText = prompt.length > 0;\n\n// reply target id (для reply_to_message_id)\nconst reply_to_message_id = (msg.message_id ?? $json.deleteMessageId ?? null);\n\n// ---------- Настройки Seedream ----------\n// Если не сохранены мини-аппом — подставим дефолты и посчитаем размеры\nfunction buildDefaultSeedream() {\n // база 2K (из доки), и масштабируем на 1k/4k\n const BASE_2K = {\n \"1:1\": { w: 2048, h: 2048 },\n \"4:3\": { w: 2304, h: 1728 },\n \"3:4\": { w: 1728, h: 2304 },\n \"16:9\": { w: 2560, h: 1440 },\n \"9:16\": { w: 1440, h: 2560 },\n \"3:2\": { w: 2496, h: 1664 },\n \"2:3\": { w: 1664, h: 2496 },\n \"21:9\": { w: 3024, h: 1296 },\n };\n const SCALES = { '1k':0.5, '2k':1.0, '4k':2.0 };\n const quality = '2k';\n const ratio = '1:1';\n const dims = BASE_2K[ratio];\n return {\n quality,\n ratio,\n dims_t2i: { w: dims.w, h: dims.h },\n long_edge_i2i: 2048, // для i2i\n savedAt: new Date().toISOString()\n };\n}\n\nconst cfg0 = sd.seedream[chatId] || buildDefaultSeedream();\n// валидируем минимально\nconst quality = ['1k','2k','4k'].includes(String(cfg0.quality)) ? cfg0.quality : '2k';\nconst ratio = [\"1:1\",\"4:3\",\"3:4\",\"16:9\",\"9:16\",\"3:2\",\"2:3\",\"21:9\"].includes(String(cfg0.ratio)) ? cfg0.ratio : '1:1';\nconst dims_t2i = (cfg0.dims_t2i && Number(cfg0.dims_t2i.w) && Number(cfg0.dims_t2i.h)) ? cfg0.dims_t2i : null;\nconst long_edge_i2i = Number(cfg0.long_edge_i2i) || (quality==='1k'?1024:(quality==='4k'?4096:2048));\n\nconst cfg = { quality, ratio, dims_t2i, long_edge_i2i, savedAt: cfg0.savedAt };\n\n// формат настройки для текста\nfunction fmtSettingsSeedream(c) {\n const q = c.quality.toUpperCase();\n const ar = c.ratio;\n const dims = c.dims_t2i ? `${c.dims_t2i.w}×${c.dims_t2i.h}` : '—';\n return [\n `🧩 Mode: auto (t2i/i2i)`,\n `🖼 Quality: ${q}`,\n `📐 Aspect (t2i): ${ar} · ${dims}`,\n `↔️ Long edge (i2i): ${c.long_edge_i2i}`\n ].join('\\n');\n}\n\n// ---------- detect incoming image ----------\nlet incoming = null;\n\n// 1) нормализованный photo_file_id\nif ($json.photo_file_id) {\n incoming = {\n file_id: $json.photo_file_id,\n kind: $json.file_kind || 'photo',\n file_name: $json.file_name || null,\n mime_type: $json.mime_type || null,\n message_id: (msg.message_id ?? $json.deleteMessageId ?? null),\n savedAt: new Date().toISOString()\n };\n}\n\n// 2) Telegram document\nif (!incoming && msg.document?.file_id) {\n const mt = String(msg.document.mime_type || '').toLowerCase();\n const name = String(msg.document.file_name || '');\n const looksImage = mt.startsWith('image/') || /\\.(png|jpe?g|webp|bmp|gif|tiff?)$/i.test(name);\n if (looksImage) {\n incoming = {\n file_id: msg.document.file_id,\n kind: 'document',\n file_name: name || null,\n mime_type: msg.document.mime_type || null,\n message_id: (msg.message_id ?? $json.deleteMessageId ?? null),\n savedAt: new Date().toISOString()\n };\n }\n}\n\n// 3) Telegram photo[]\nif (!incoming && Array.isArray(msg.photo) && msg.photo.length) {\n const best = msg.photo[msg.photo.length - 1];\n incoming = {\n file_id: best.file_id,\n kind: 'photo',\n file_name: null,\n mime_type: null,\n message_id: (msg.message_id ?? $json.deleteMessageId ?? null),\n savedAt: new Date().toISOString()\n };\n}\n\n// ---------- guards & commands ----------\nif (!chatId) {\n return [{ json: { action: 'ERROR', reason: 'no_chat', got: Object.keys($json), reply_to_message_id } }];\n}\n\n// /clear очистка корзины\nif (/^\\/clear\\b/i.test(prompt)) {\n delete sd.photoBucket[chatId];\n return [{ json: { action:'CLEARED', chatId, reply:'🗑️ Копилка фото очищена.', settings_seedream: cfg, settings_text: fmtSettingsSeedream(cfg), reply_to_message_id } }];\n}\n\n// ---------- helpers ----------\nfunction ensureBucket() { return sd.photoBucket[chatId] ?? { files: [], savedAt: new Date().toISOString() }; }\nfunction pushLimited(b, f, limit=10) {\n const exists = (b.files || []).some(x => x.file_id === f.file_id);\n if (!exists) {\n (b.files = b.files || []).push(f);\n if (b.files.length > limit) b.files = b.files.slice(-limit);\n }\n return b;\n}\nfunction kbClearBucket() {\n return [[ { text: '🗑 Очистить', callback_data: 'seedream:clear' } ]];\n}\n\n// ====================================================\n// ===================== FLOW =========================\n// ====================================================\n\n// 1) Фото БЕЗ текста → STORED (i2i будет возможен после текста)\nif (incoming && !hasText) {\n const bucket = pushLimited(ensureBucket(), incoming, 10);\n sd.photoBucket[chatId] = bucket;\n return [{\n json: {\n action: 'STORED',\n chatId,\n tool,\n storedCount: bucket.files.length,\n reply: `📥 Фото сохранено (${bucket.files.length}). Теперь отправьте текст — запущу i2i.\\n\\n${fmtSettingsSeedream(cfg)}`,\n inline_keyboard: kbClearBucket(),\n settings_seedream: cfg,\n settings_text: fmtSettingsSeedream(cfg),\n reply_to_message_id\n }\n }];\n}\n\n// 2) Решение по запуску\nconst bucket = sd.photoBucket[chatId] || { files: [] };\nconst files = bucket.files || [];\nconst hasStoredPhoto = files.length > 0;\n\n// Если есть текст и НЕТ фото → t2i\nif (hasText && !hasStoredPhoto && !incoming) {\n // ожидается генерация t2i: из настроек отдаём dims_t2i\n return [{\n json: {\n action: 'GENERATE',\n mode: 't2i',\n chatId,\n tool,\n prompt,\n files: [],\n wants_base64: false,\n clearAfter: false,\n next: 'BUILD_T2I_REQUEST',\n // t2i параметры из настроек (если null — посчитаешь дальше)\n t2i_dims: cfg.dims_t2i || null, // {w,h} или null\n t2i_ratio: cfg.ratio, // '1:1' ...\n t2i_quality: cfg.quality, // '1k'|'2k'|'4k'\n settings_seedream: cfg,\n settings_text: fmtSettingsSeedream(cfg),\n reply_to_message_id\n }\n }];\n}\n\n// /clear очистка корзины\nif (/^\\/clear\\b/i.test(prompt)) {\n delete sd.photoBucket[chatId];\n return [{\n json: {\n action: 'CLEARED',\n chatId,\n reply: '🗑️ Копилка фото очищена.',\n settings_seedream: cfg,\n settings_text: fmtSettingsSeedream(cfg),\n reply_to_message_id\n }\n }];\n}\n\n// Если есть текст и ЕСТЬ фото (или пришло вместе) → i2i\nif (hasText && (hasStoredPhoto || incoming)) {\n const all = hasStoredPhoto ? files.slice() : [];\n if (incoming) all.push(incoming);\n return [{\n json: {\n action: 'GENERATE',\n mode: 'i2i',\n chatId,\n tool,\n prompt,\n files: all, // массив {file_id,...}\n primary_file: all[all.length-1] || null,\n wants_base64: true,\n clearAfter: true, // после старта очищаем корзину\n next: 'FETCH_FILE_BASE64',\n // i2i целевая \"длинная сторона\"\n i2i_long_edge: cfg.long_edge_i2i,\n settings_seedream: cfg,\n settings_text: fmtSettingsSeedream(cfg),\n reply_to_message_id\n }\n }];\n}\n\n// Если текста нет и фото нет → подсказка\nif (!hasText && !incoming && !hasStoredPhoto) {\n return [{\n json: {\n action: 'REMIND',\n chatId,\n tool,\n reply: `Пришлите текст — запущу t2i, или пришлите фото, затем текст — запущу i2i.\\n\\n${fmtSettingsSeedream(cfg)}`,\n settings_seedream: cfg,\n settings_text: fmtSettingsSeedream(cfg),\n reply_to_message_id\n }\n }];\n}\n\n// Фолбек (например, пришло фото и ещё фото без текста)\nreturn [{\n json: {\n action: 'NOOP',\n chatId,\n tool,\n reply: `Отправьте текст для запуска.\\n\\n${fmtSettingsSeedream(cfg)}`,\n settings_seedream: cfg,\n settings_text: fmtSettingsSeedream(cfg),\n reply_to_message_id\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1664,
3072
],
"id": "ef8f52dd-7dba-451a-a8e3-12cff8aa7384",
"name": "Seedream — photo store & launch"
},
{
"parameters": {
"jsCode": "// Seedream — builder for Comet /v1/images/generations (t2i: WIDTHxHEIGHT, i2i: 1k/2k/4k)\n\nconst MODEL = 'bytedance-seedream-4-0-250828';\n\n// 1) Входы\nconst cfg = $json.settings_seedream || {};\nconst prompt = String($json.prompt || '').trim();\nconst n = Number.isFinite(Number($json.n)) ? Number($json.n) : 1;\n\n// helpers\nfunction isDataUrl(u){ return typeof u === 'string' && /^data:image\\/[a-z0-9.+-]+;base64,/i.test(u); }\nfunction isHttp(u){ return typeof u === 'string' && /^https?:\\/\\//i.test(u); }\nfunction extractImages(src){\n const out = [];\n if (!src) return out;\n if (Array.isArray(src)) {\n for (const it of src) {\n if (typeof it === 'string' && (isDataUrl(it) || isHttp(it))) out.push(it);\n else if (it && typeof it === 'object') {\n const cand = it.dataUrl || it.url || it.data || it.image_url || it.imageUrl;\n if (typeof cand === 'string' && (isDataUrl(cand) || isHttp(cand))) out.push(cand);\n }\n }\n } else if (typeof src === 'string' && (isDataUrl(src) || isHttp(src))) {\n out.push(src);\n } else if (src && typeof src === 'object') {\n const cand = src.dataUrl || src.url || src.data || src.image_url || src.imageUrl;\n if (typeof cand === 'string' && (isDataUrl(cand) || isHttp(cand))) out.push(cand);\n }\n return out;\n}\n\n// 2) Соберём изображения (если есть — это i2i)\nlet images = [];\nimages = images.concat(extractImages($json.dataUrls));\nimages = images.concat(extractImages($json.images));\nimages = images.concat(extractImages($json.dataUrl));\nimages = images.concat(extractImages($json.image_url));\nimages = Array.from(new Set(images));\n\nconst isI2I = images.length > 0;\n\n// 3) Определяем size\nlet sizeValue;\n\n// t2i → WIDTHxHEIGHT\nif (!isI2I) {\n const dims = $json.t2i_dims || cfg.dims_t2i || null;\n let w = Number(dims?.w), h = Number(dims?.h);\n if (!(Number.isFinite(w) && Number.isFinite(h) && w>0 && h>0)) {\n // fallback по качеству/соотношению, если вдруг dims не пришли\n // по умолчанию 2k квадрат\n const q = (cfg.quality || '2k').toLowerCase();\n if (q === '1k') { w = 1024; h = 1024; }\n else if (q === '4k') { w = 4096; h = 4096; }\n else { w = 2048; h = 2048; }\n }\n sizeValue = `${Math.round(w)}x${Math.round(h)}`;\n} else {\n // i2i → '1k'|'2k'|'4k' (как раньше)\n let size = (cfg.quality || '2k').toLowerCase();\n if (!['1k','2k','4k'].includes(size)) size = '2k';\n sizeValue = size;\n}\n\n// 4) Сборка payload\nconst body = {\n model: MODEL,\n prompt: prompt,\n n: n,\n size: sizeValue,\n response_format: 'url',\n watermark: false\n};\nif (isI2I) body.image = images;\n\n// 5) Выход\nreturn [{\n json: {\n method: 'POST',\n url: 'https://api.cometapi.com/v1/images/generations',\n headers: { 'Content-Type': 'application/json' },\n body\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2384,
3440
],
"id": "3ef7d833-7c59-4a20-b67b-8461aa027bf7",
"name": "Нормализовать task_id1",
"alwaysOutputData": false
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.action }}",
"rightValue": "STORED",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.action }}",
"rightValue": "=GENERATE",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "9cff2c53-bb3e-49bf-9cfe-53963eac8978",
"leftValue": "={{ $json.action }}",
"rightValue": "CLEARED",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "39ccebaf-cc95-4d0b-954f-71fab6178691",
"leftValue": "={{ $json.action }}",
"rightValue": "NOOP ",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "a9f7ce9b-cd18-4138-83f1-8cb70c4643ed",
"leftValue": "={{ $json.action }}",
"rightValue": "REMIND",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
2032,
3024
],
"id": "8f1d2ee7-ebaa-44be-996b-958adf1fbb1f",
"name": "Switch"
},
{
"parameters": {
"jsCode": "const sd = $getWorkflowStaticData('global');\nsd.photoIndex = sd.photoIndex || {}; // { [chatId]: { [messageId]: file_id } }\n\nconst chatId = String($json.chatId || '');\nconst msgId = Number($json.photo_message_id ?? $json.deleteMessageId ?? 0);\nconst fileId = $json.file_id || $json.photo_file_id || $json.primary_file?.file_id || null;\n\n\nif (chatId && msgId && fileId) {\n sd.photoIndex[chatId] = sd.photoIndex[chatId] || {};\n sd.photoIndex[chatId][msgId] = fileId;\n}\n\nreturn [{ json: $json }];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2336,
2976
],
"id": "4792537f-3ed4-4206-8839-bab0460b5cdf",
"name": "Code"
},
{
"parameters": {
"jsCode": "const sd = $getWorkflowStaticData('global');\nsd.replyIndex = sd.replyIndex || {}; // { [chatId]: number[] }\n\nconst chatId = String($json.chatId || $json.result?.chat?.id || '');\nconst sentId =\n Number($json.result?.message_id) ||\n Number($json.result?.message?.message_id) ||\n Number($json.message_id) || 0;\n\nif (chatId && sentId) {\n sd.replyIndex[chatId] = sd.replyIndex[chatId] || [];\n if (!sd.replyIndex[chatId].includes(sentId)) sd.replyIndex[chatId].push(sentId);\n}\n\nreturn [{ json: $json }];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2672,
2976
],
"id": "c566fc05-b832-407f-91a0-e1b2570df49c",
"name": "Code24"
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.lang === 'en'\n ? `${$json.reply}\\n⚡ Generation cost: ${$json.price} credits.\\nTap a button to delete.`\n : `${$json.reply}\\n⚡Стоимость генерации: ${$json.price} кредитов.\\nНажмите кнопку, чтобы удалить.`\n}}",
"replyMarkup": "inlineKeyboard",
"inlineKeyboard": {
"rows": [
{
"row": {
"buttons": [
{
"text": "={{ $json.lang === 'en'\n ? '🗑 Delete this photo'\n : '🗑 Удалить это фото'\n}}",
"additionalFields": {
"callback_data": "del:${$json.photo_message_id}"
}
}
]
}
},
{
"row": {
"buttons": [
{
"text": "={{ $json.lang === 'en'\n ? '🧺 Clear all'\n : '🧺 Очистить все'\n}}",
"additionalFields": {
"callback_data": "del_all"
}
}
]
}
}
]
},
"additionalFields": {
"appendAttribution": false,
"reply_to_message_id": "={{ $json.reply_to_message_id }}"
}
},
"id": "8cc27531-b8e6-4985-a60b-0826c028b6f9",
"name": "Telegram",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2496,
2976
],
"webhookId": "6969c97f-2c91-44e1-ae46-426d11c99897",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
}
],
"connections": {
"Switch1": {
"main": [
[
{
"node": "Switch_lang_act",
"type": "main",
"index": 0
}
],
[
{
"node": "lang_inline",
"type": "main",
"index": 0
}
],
[
{
"node": "Instruct_mes",
"type": "main",
"index": 0
}
],
[
{
"node": "Seedream — photo store & launch",
"type": "main",
"index": 0
}
]
]
},
"Telegram Trigger1": {
"main": [
[
{
"node": "Switch_message_type",
"type": "main",
"index": 0
}
]
]
},
"reply_markup": {
"main": [
[
{
"node": "deleteMessageMenu",
"type": "main",
"index": 0
}
]
]
},
"menu": {
"main": [
[
{
"node": "switch_start",
"type": "main",
"index": 0
}
]
]
},
"build_sub_invoice": {
"main": [
[
{
"node": "createInvoiceLink",
"type": "main",
"index": 0
}
]
]
},
"payment.precheckout.parse": {
"main": [
[
{
"node": "answerPreCheckoutQuery",
"type": "main",
"index": 0
}
]
]
},
"createInvoiceLink": {
"main": [
[
{
"node": "send_mes_inline_buy_plan",
"type": "main",
"index": 0
},
{
"node": "delMes1",
"type": "main",
"index": 0
}
]
]
},
"answerPreCheckoutQuery": {
"main": [
[]
]
},
"payment.parse_success": {
"main": [
[
{
"node": "apply_payment",
"type": "main",
"index": 0
},
{
"node": "payment_pick_prompt_to_delete",
"type": "main",
"index": 0
}
]
]
},
"apply_payment": {
"main": [
[
{
"node": "send_mes_payment",
"type": "main",
"index": 0
}
]
]
},
"Switch_message_type": {
"main": [
[
{
"node": "payment.precheckout.parse",
"type": "main",
"index": 0
}
],
[
{
"node": "payment.parse_success",
"type": "main",
"index": 0
}
],
[
{
"node": "message_reply_inline",
"type": "main",
"index": 0
}
],
[
{
"node": "Switch_web_app",
"type": "main",
"index": 0
}
]
]
},
"Switch_web_app": {
"main": [
[
{
"node": "parse web_app_data",
"type": "main",
"index": 0
}
],
[
{
"node": "menu",
"type": "main",
"index": 0
}
]
]
},
"switch_reply_menu": {
"main": [
[
{
"node": "reply_markup",
"type": "main",
"index": 0
}
],
[
{
"node": "Switch1",
"type": "main",
"index": 0
}
]
]
},
"parse web_app_data": {
"main": [
[]
]
},
"lang_inline": {
"main": [
[
{
"node": "delMes",
"type": "main",
"index": 0
}
]
]
},
"delMes_keyboardMsg1": {
"main": [
[]
]
},
"Switch_lang_act": {
"main": [
[
{
"node": "id_and_balance",
"type": "main",
"index": 0
}
],
[
{
"node": "send_mes_plan",
"type": "main",
"index": 0
}
],
[
{
"node": "build_sub_invoice",
"type": "main",
"index": 0
}
],
[
{
"node": "build_credits_packs",
"type": "main",
"index": 0
}
]
]
},
"message_reply_inline": {
"main": [
[
{
"node": "Switch_inline_act",
"type": "main",
"index": 0
}
]
]
},
"Switch_inline_act": {
"main": [
[
{
"node": "delMes_targetMsg",
"type": "main",
"index": 0
},
{
"node": "delMes_keyboardMsg",
"type": "main",
"index": 0
},
{
"node": "Delete ONE image from memory",
"type": "main",
"index": 0
}
],
[
{
"node": "Build items-to-delete",
"type": "main",
"index": 0
}
],
[
{
"node": "SB_set_user_lang",
"type": "main",
"index": 0
}
],
[
{
"node": "build_credits_invoice",
"type": "main",
"index": 0
},
{
"node": "delMes_keyboardMsg2",
"type": "main",
"index": 0
}
]
]
},
"SB_set_user_lang": {
"main": [
[
{
"node": "delMes_keyboardMsg1",
"type": "main",
"index": 0
},
{
"node": "send_mes_lang_save",
"type": "main",
"index": 0
}
]
]
},
"Build items-to-delete": {
"main": [
[
{
"node": "delMes2",
"type": "main",
"index": 0
},
{
"node": "Clear ALL buckets/indexes",
"type": "main",
"index": 0
}
]
]
},
"send_mes_balance": {
"main": [
[
{
"node": "delMes1",
"type": "main",
"index": 0
}
]
]
},
"send_mes_plan": {
"main": [
[
{
"node": "delMes1",
"type": "main",
"index": 0
}
]
]
},
"Clear ALL buckets/indexes": {
"main": [
[
{
"node": "send_mes_clear",
"type": "main",
"index": 0
}
]
]
},
"build_credits_packs": {
"main": [
[
{
"node": "reply_markup1",
"type": "main",
"index": 0
}
]
]
},
"createInvoiceLink_credits": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"reply_markup1": {
"main": [
[
{
"node": "delMes1",
"type": "main",
"index": 0
}
]
]
},
"build_credits_invoice": {
"main": [
[
{
"node": "createInvoiceLink_credits",
"type": "main",
"index": 0
},
{
"node": "create_payment_yookassa_credits",
"type": "main",
"index": 0
}
]
]
},
"send_mes_payment1": {
"main": [
[
{
"node": "payment_store_prompt",
"type": "main",
"index": 0
},
{
"node": "store_payment_message",
"type": "main",
"index": 0
}
]
]
},
"payment_pick_prompt_to_delete": {
"main": [
[
{
"node": "delMes3",
"type": "main",
"index": 0
}
]
]
},
"welcome_once": {
"main": [
[
{
"node": "switch_welcome",
"type": "main",
"index": 0
}
]
]
},
"switch_start": {
"main": [
[
{
"node": "ensure_user",
"type": "main",
"index": 0
}
],
[
{
"node": "switch_reply_menu",
"type": "main",
"index": 0
}
]
]
},
"switch_welcome": {
"main": [
[
{
"node": "send_wel_cred",
"type": "main",
"index": 0
}
]
]
},
"send_mes_lang_save": {
"main": [
[
{
"node": "lang_to_home_after_set",
"type": "main",
"index": 0
}
]
]
},
"lang_to_home_after_set": {
"main": [
[
{
"node": "Switch_message_type",
"type": "main",
"index": 0
}
]
]
},
"delMes": {
"main": [
[]
]
},
"delMes4": {
"main": [
[
{
"node": "lang_to_home_after_set1",
"type": "main",
"index": 0
}
]
]
},
"lang_to_home_after_set1": {
"main": [
[
{
"node": "Switch_message_type",
"type": "main",
"index": 0
}
]
]
},
"Instruct_mes": {
"main": [
[
{
"node": "delMes4",
"type": "main",
"index": 0
}
]
]
},
"switch_home_inline": {
"main": [
[
{
"node": "send_home_inline",
"type": "main",
"index": 0
}
]
]
},
"Merge": {
"main": [
[
{
"node": "send_mes_payment1",
"type": "main",
"index": 0
}
]
]
},
"create_payment_yookassa_credits": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"id_and_balance": {
"main": [
[
{
"node": "send_mes_balance",
"type": "main",
"index": 0
}
]
]
},
"ensure_user": {
"main": [
[
{
"node": "welcome_once",
"type": "main",
"index": 0
},
{
"node": "send_start_mes",
"type": "main",
"index": 0
}
]
]
},
"Switch2": {
"main": [
[
{
"node": "Code15",
"type": "main",
"index": 0
}
],
[
{
"node": "Call FLUX",
"type": "main",
"index": 0
}
],
[
{
"node": "Telegram19",
"type": "main",
"index": 0
}
],
[
{
"node": "Telegram18",
"type": "main",
"index": 0
}
],
[
{
"node": "Telegram18",
"type": "main",
"index": 0
}
]
]
},
"Telegram20": {
"main": [
[
{
"node": "Code23",
"type": "main",
"index": 0
}
]
]
},
"Code15": {
"main": [
[
{
"node": "Telegram20",
"type": "main",
"index": 0
}
]
]
},
"Flux settings": {
"main": [
[
{
"node": "flux_price_and_aspect",
"type": "main",
"index": 0
}
]
]
},
"flux_price_and_aspect": {
"main": [
[
{
"node": "Switch2",
"type": "main",
"index": 0
}
]
]
},
"Call FLUX": {
"main": [
[
{
"node": "Clean_memory",
"type": "main",
"index": 0
}
]
]
},
"Seedream — photo store & launch": {
"main": [
[
{
"node": "Switch",
"type": "main",
"index": 0
}
]
]
},
"Switch": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"Code": {
"main": [
[
{
"node": "Telegram",
"type": "main",
"index": 0
}
]
]
},
"Telegram": {
"main": [
[
{
"node": "Code24",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"timezone": "Asia/Bangkok",
"callerPolicy": "workflowsFromSameOwner",
"availableInMCP": false,
"errorWorkflow": "I7DgFold1DUWIFbw"
},
"staticData": {
"global": {
"menu": {
"140652": {
"path": "account",
"updatedAt": "2025-12-07T19:05:09.927Z",
"lang": "ru"
},
"748140117": {
"path": "image/flux",
"updatedAt": "2025-12-10T02:40:42.956Z",
"lang": "en"
},
"1110290411": {
"path": "",
"updatedAt": "2025-12-09T14:25:21.452Z",
"lang": "en"
},
"8159721308": {
"path": "image/seedream",
"updatedAt": "2025-11-09T06:52:54.244Z",
"lang": "ru"
},
"7103210852": {
"path": "",
"updatedAt": "2025-12-03T11:02:01.883Z",
"lang": "ru"
},
"7108317408": {
"path": "account",
"updatedAt": "2025-12-06T12:00:30.536Z",
"lang": "ru"
}
},
"session": {
"140652": {
"tool": "account"
},
"748140117": {
"tool": "flux"
},
"1110290411": {
"tool": null
},
"8159721308": {
"tool": "seedream"
},
"7103210852": {
"tool": null
},
"7108317408": {
"tool": "account"
}
},
"photoBucket": {
"748140117": {
"files": [
{
"file_id": "AgACAgIAAxkBAAIRiWk4fw_3W-V_Xp5XVrSEPtdRHu76AALbDWsbm_rJSeEcAgmY6FB7AQADAgADeQADNgQ",
"kind": "photo",
"file_name": null,
"mime_type": null,
"message_id": 4489,
"savedAt": "2025-12-09T19:50:58.332Z"
},
{
"file_id": "AgACAgIAAxkBAAIRi2k4f2KEws_bOImu98vR8cjPOUUTAALeDWsbm_rJSdfDFsK7ry86AQADAgADeQADNgQ",
"kind": "photo",
"file_name": null,
"mime_type": null,
"message_id": 4493,
"savedAt": "2025-12-09T19:54:47.567Z"
}
],
"savedAt": "2025-12-09T19:50:58.332Z"
}
},
"nanobanana": {},
"photoIndex": {},
"replyIndex": {
"748140117": [
4494
]
},
"photoBucket_seedance": {},
"photoIndex_seedance": {},
"replyIndex_seedance": {},
"poll": {},
"veo": {
"748140117": {
"model": "veo3.1",
"ratio": "9:16",
"mode": "first_last",
"savedAt": "2025-11-04T19:03:47.865Z"
}
},
"flux": {
"748140117": {
"ar": "3:4",
"ref": 0,
"seed": 1,
"quality": 2,
"batch": 1,
"useRef": false,
"savedAt": "2025-12-09T19:52:13.008Z"
},
"1110290411": {
"ar": "1:1",
"ref": 0,
"seed": 2053943465563945,
"quality": 1,
"useRef": false,
"savedAt": "2025-12-07T19:12:57.350Z"
}
},
"seedance": {
"748140117": {
"mode": "i2v",
"tier": "lite",
"model": "bytedance-seedance-1-0-lite-i2v-250428",
"i2v_mode": "first_last",
"resolution": "720p",
"ratio": "auto",
"duration": 5,
"camerafixed": false,
"savedAt": "2025-11-04T18:33:27.501Z"
}
},
"seedream": {
"748140117": {
"quality": "2k",
"ratio": "3:2",
"dims_t2i": {
"w": 2496,
"h": 1664
},
"long_edge_i2i": 2048,
"savedAt": "2025-12-09T22:42:50.547Z"
}
},
"sora": {
"748140117": {
"model": "sora-2",
"ratio": "9:16",
"duration": 4,
"savedAt": "2025-10-18T05:21:22.904Z"
}
},
"userLang": {
"140652": "ru",
"748140117": "en",
"1110290411": "en",
"8159721308": "ru",
"7103210852": "ru",
"7108317408": "ru"
},
"userSub": {},
"photoBucket_flux": {
"748140117": {
"files": [
{
"file_id": "BQACAgIAAxkBAAILP2kVTaNKYG4mB-SIaQOCq74WabQxAAJOgwACTsuwSOtXrRWiOuEhNgQ",
"kind": "document",
"file_name": "file_1.jpg",
"mime_type": "image/jpeg",
"message_id": 2879,
"savedAt": "2025-11-13T03:11:17.163Z"
},
{
"file_id": "AgACAgIAAxkBAAILRmkVVIP7r4sEnqB8NrgSKHHErTlkAAIXC2sbTsuwSCCIYRNZzyf7AQADAgADeQADNgQ",
"kind": "photo",
"file_name": null,
"mime_type": null,
"message_id": 2886,
"savedAt": "2025-11-13T03:40:36.836Z"
}
],
"savedAt": "2025-11-13T03:11:17.163Z"
}
},
"payments": {
"stxSeN0lqz1v6MVtol4O0B1ptYsFbhkGq9MEItzInBlxW6BO_k_fQK85Xoeiv4-SvDOBqeAPCaSuk7_DZRKyXcVehs5-8GRDbzdowYOc-7NYX1Dmnlb1wS7KT7Kp9RvABtR": true,
"stxNEHIte1HGZLuft4lE3RN42bS-lTTnurc-TnKgq7-0SD-_86bEmO8b1wCSbCK6gNCKp9-Xgk4iqpT9T_knCgWWBzx7vREnapp-M_cR35yaAJl3uWr3RigtxwdZZWUvc5L": true,
"stxpNKZQkVGFslWa364AkWWE_iUUGdJrn3D95cSScRdEYqic6goUPMdOnwjGNnp2tOCbNcSY_6B2mAOnuRBNT0yrshn2ViPJv7cVk08U2-cwLxjNDCw2sYJH9czIkkiWJ5j": true
},
"paymentPrompt": {
"140652": 4017,
"748140117": 4115
}
}
},
"meta": {
"templateCredsSetupCompleted": true
},
"pinData": {
"Telegram Trigger1": [
{
"json": {
"update_id": 460304190,
"message": {
"message_id": 4556,
"from": {
"id": 748140117,
"is_bot": false,
"first_name": "Grigoriy",
"last_name": "Voyakin",
"username": "GRIGORIY_VOYAKIN",
"language_code": "ru"
},
"chat": {
"id": 748140117,
"first_name": "Grigoriy",
"last_name": "Voyakin",
"username": "GRIGORIY_VOYAKIN",
"type": "private"
},
"date": 1765320535,
"web_app_data": {
"button_text": "⚙️ Settings",
"data": "{\"type\":\"seedream_settings\",\"model\":\"4.0\",\"quality\":\"2k\",\"ratio\":\"3:2\",\"dims_t2i\":{\"w\":2496,\"h\":1664},\"long_edge_i2i\":2048}"
}
}
}
}
]
},
"versionId": "9ff68b38-0758-4e45-a6e0-011208f9f4c4",
"activeVersionId": "9ff68b38-0758-4e45-a6e0-011208f9f4c4",
"versionCounter": 935,
"triggerCount": 1,
"shared": [
{
"updatedAt": "2025-12-09T17:22:27.440Z",
"createdAt": "2025-12-09T17:22:27.440Z",
"role": "workflow:owner",
"workflowId": "HfvL0uSdAtwjBN7u",
"projectId": "MHclKTSzdRCLxmxU",
"project": {
"updatedAt": "2025-05-06T12:49:51.317Z",
"createdAt": "2025-05-06T12:48:38.577Z",
"id": "MHclKTSzdRCLxmxU",
"name": "Grigoriy Voyakin <grigoriyvoyakinwork@gmail.com>",
"type": "personal",
"icon": null,
"description": null,
"projectRelations": [
{
"updatedAt": "2025-05-06T12:48:38.577Z",
"createdAt": "2025-05-06T12:48:38.577Z",
"userId": "15659665-1e18-4119-8f87-b50a3cb257b7",
"projectId": "MHclKTSzdRCLxmxU",
"user": {
"updatedAt": "2025-12-10T02:57:46.000Z",
"createdAt": "2025-05-06T12:48:38.340Z",
"id": "15659665-1e18-4119-8f87-b50a3cb257b7",
"email": "grigoriyvoyakinwork@gmail.com",
"firstName": "Grigoriy",
"lastName": "Voyakin",
"personalizationAnswers": {
"version": "v4",
"personalization_survey_submitted_at": "2025-05-09T12:49:28.002Z",
"personalization_survey_n8n_version": "1.91.3"
},
"settings": {
"userActivated": true,
"easyAIWorkflowOnboarded": true,
"firstSuccessfulWorkflowId": "6ndVsjNc12yZ7vD6",
"userActivatedAt": 1747397744116,
"npsSurvey": {
"responded": true,
"lastShownAt": 1756144477136
}
},
"disabled": false,
"mfaEnabled": false,
"lastActiveAt": "2025-12-09",
"isPending": false
}
}
]
}
}
],
"tags": [
{
"updatedAt": "2025-10-29T09:15:49.403Z",
"createdAt": "2025-10-29T09:15:49.403Z",
"id": "NFsjRzvK9b3zMBTF",
"name": "docs"
}
],
"activeVersion": {
"updatedAt": "2025-12-10T02:57:21.280Z",
"createdAt": "2025-12-10T02:57:21.280Z",
"versionId": "9ff68b38-0758-4e45-a6e0-011208f9f4c4",
"workflowId": "HfvL0uSdAtwjBN7u",
"nodes": [
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "78542b6d-15c2-4809-935c-49d1e17dcc1e",
"leftValue": "={{ $json.tool }}",
"rightValue": "account",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "dfbae9da-3cc7-488e-9f71-f21a4b9b4345",
"leftValue": "={{ $json.tool }}",
"rightValue": "lang",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "19714412-73ff-4c46-ba9e-4f4276157e72",
"leftValue": "={{ $json.tool }}",
"rightValue": "instruction",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.tool }}",
"rightValue": "=flux",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
1328,
2016
],
"id": "f6103241-cbad-4932-a4e4-d845d2eb65c2",
"name": "Switch1"
},
{
"parameters": {
"updates": [
"message",
"callback_query",
"pre_checkout_query"
],
"additionalFields": {}
},
"id": "04d5c88f-05c1-49e8-b34b-26ceb2ab22d2",
"name": "Telegram Trigger1",
"type": "n8n-nodes-base.telegramTrigger",
"typeVersion": 1,
"position": [
-1568,
1952
],
"webhookId": "aaa5322f-da8f-4d75-b626-ae10bb379fbe",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"method": "POST",
"url": "https://api.telegram.org/bot8495842221:AAHBriXvzudyazB2f8z3ilGQ8A5ESdjjtl8/sendMessage",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "chat_id",
"value": "={{ $json.chatId }}"
},
{
"name": "text",
"value": "={{ $json.replyText }}"
},
{
"name": "parse_mode",
"value": "HTML"
},
{
"name": "reply_markup",
"value": "={{ $json.replyMarkup }}"
},
{
"name": "disable_notification",
"value": "={{ $json.silent }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
624,
1792
],
"id": "69d935ba-a076-4654-bc52-a14458e6180e",
"name": "reply_markup",
"retryOnFail": true
},
{
"parameters": {
"jsCode": "// === MENU (RU/EN localization) ===\n// Keep original logic; add Account section + \"flat\" actions inside Account\n\n// ---- state\nconst sd = $getWorkflowStaticData('global');\nsd.menu = sd.menu || {};\nsd.session = sd.session || {};\nsd.userLang = sd.userLang || {};\nsd.userSub = sd.userSub || {}; // { [chatId]: { active: boolean } } optional cache\n\n// ---- input\nconst msg = $json.message || {};\nconst chatId = String(msg.chat?.id || $json.chat?.id || '');\nconst msgId = msg.message_id;\nconst textIn = String(msg.text ?? msg.caption ?? '').trim();\n\n// ---- language resolve: 1) payload.lang 2) cached 3) Telegram UI hint 4) ru\nlet lang = (typeof $json.lang === 'string' && /^(ru|en)$/i.test($json.lang)) ? $json.lang.toLowerCase() : null;\nif (!lang) lang = sd.userLang[chatId] || null;\nif (!lang) lang = (/^en/i.test(String(msg.from?.language_code || ''))) ? 'en' : 'ru';\nsd.userLang[chatId] = lang;\n\n// ---- localization dict\nconst I18N = {\n ru: {\n home: '🏠 Главная',\n back: '⬅️ Назад',\n account: '👤 Аккаунт',\n mainTitle: '🏠 Главная страница',\n mainDesc: 'Выберите инструмент',\n help: '❔ Помощь',\n helpDesc: 'Раздел помощи',\n helpInstruction: '📘 Инструкция',\n helpInstructionDesc: 'Как пользоваться ботом',\n language: '🌐 Язык',\n languageDesc: 'Выбор языка интерфейса',\n image: '🖼 Image',\n imageDesc: 'Генерация изображений',\n gpt: '💬 GPT',\n gptDesc: 'Чат с LLM',\n audio: '🎵 Audio',\n audioDesc: 'Аудио-инструменты',\n video: '🎬 Video',\n videoDesc: 'Видео-инструменты',\n seedream: '🌊 Seedream',\n nanobanana: '🍌 Nano Banana',\n flux: '⚡️ FLUX',\n fluxDesc: 'Генерация изображения с FLUX',\n seedance: '🎞 Seedance',\n veo: '⭕ Veo',\n sora: '🌙 Sora 2',\n settings: '⚙️ Settings',\n placeholder: 'Меню',\n\n // Account\n accTitle: '👤 Аккаунт',\n accDesc: 'Управление аккаунтом',\n accBalance: '💰 Баланс',\n accStatus: '📦 Статус',\n accBuySub: '💳 Купить подписку',\n accBuyCreds: '⚡️ Купить кредиты',\n\n // NEW: Community\n community: '👥 Сообщество',\n communityDesc: 'Ссылка на канал @voyakin_geni',\n },\n en: {\n home: '🏠 Home',\n back: '⬅️ Back',\n account: '👤 Account',\n mainTitle: '🏠 Home',\n mainDesc: 'Pick a tool',\n help: '❔ Help',\n helpDesc: 'Help section',\n helpInstruction: '📘 Instructions',\n helpInstructionDesc: 'How to use the bot',\n language: '🌐 Language',\n languageDesc: 'Choose interface language',\n image: '🖼 Image',\n imageDesc: 'Image generation',\n gpt: '💬 GPT',\n gptDesc: 'Chat with LLM',\n audio: '🎵 Audio',\n audioDesc: 'Audio tools',\n video: '🎬 Video',\n videoDesc: 'Video tools',\n seedream: '🌊 Seedream',\n nanobanana: '🍌 Nano Banana',\n flux: '⚡️ FLUX',\n fluxDesc: 'Generating an image with FLUX',\n seedance: '🎞 Seedance',\n veo: '⭕ Veo',\n sora: '🌙 Sora 2',\n settings: '⚙️ Settings',\n placeholder: 'Menu',\n\n // Account\n accTitle: '👤 Account',\n accDesc: 'Manage your account',\n accBalance: '💰 Balance',\n accStatus: '📦 Status',\n accBuySub: '💳 Buy subscription',\n accBuyCreds: '⚡️ Buy credits',\n\n // NEW: Community\n community: '👥 Community',\n communityDesc: 'Link to channel @voyakin_geni',\n }\n};\nconst L = (k)=> (I18N[lang]||I18N.ru)[k] ?? k;\n\n// ---- webapps\nconst WEBAPP_ACCOUNT = 'https://tgmenu.pages.dev/account';\nconst WEBAPP_SETTINGS = 'https://app-ru.geni-ai.online/settings';\n\n// ---- menu tree (localized)\nconst MENU = {\n key: '', title: L('mainTitle'), desc: L('mainDesc'), children: [\n { key: 'image', title: L('image'), desc: L('imageDesc'), children: [\n // [V1 HIDE] Seedream / Nano Banana временно отключены\n { key: 'seedream', title: L('seedream'), desc: '—', command: 'seedream', children: [\n { key: 'settings', title: L('settings'), type: 'webapp', webUrl: WEBAPP_SETTINGS + '/seedream.html?tool=seedream' },\n ]},\n // { key: 'nano banana', title: L('nanobanana'), desc: '—', command: 'nano banana', children: [\n // { key: 'settings', title: L('settings'), type: 'webapp', webUrl: WEBAPP_SETTINGS + '/nanobanana.html' },\n // ]},\n { key: 'flux', title: L('flux'), desc: L('fluxDesc'), command: 'flux', children: [\n { key: 'settings', title: L('settings'), type: 'webapp', webUrl: WEBAPP_SETTINGS + '/flux3.html?tool=flux' },\n ]},\n ]},\n\n // [V1 HIDE] GPT / Audio / Video временно отключены\n // { key: 'gpt', title: L('gpt'), desc: L('gptDesc'), command: 'gpt' },\n // { key: 'audio', title: L('audio'), desc: L('audioDesc'), command: 'audio' },\n // { key: 'video', title: L('video'), desc: L('videoDesc'), command: 'video', children: [\n // { key: 'seedance', title: L('seedance'), desc: '—', command: 'seedance', children: [\n // { key: 'settings', title: L('settings'), type: 'webapp', webUrl: WEBAPP_SETTINGS + '/seedance.html' }\n // ]},\n // { key: 'veo', title: L('veo'), desc: '—', command: 'veo', children: [\n // { key: 'settings', title: L('settings'), type: 'webapp', webUrl: WEBAPP_SETTINGS + '/veo.html' }\n // ]},\n // { key: 'sora', title: L('sora'), desc: '—', command: 'sora', children: [\n // { key: 'settings', title: L('settings'), type: 'webapp', webUrl: WEBAPP_SETTINGS + '/sora_2.html' }\n // ]},\n // ]},\n\n { key: 'help', title: L('help'), desc: L('helpDesc'), children: [\n { key: 'language', title: L('language'), desc: L('languageDesc'), command: 'lang' },\n { key: 'instruction', title: L('helpInstruction'), desc: L('helpInstructionDesc'), command: 'instruction' },\n ]},\n\n // ACCOUNT (top-level)\n { key: 'account', title: L('accTitle'), desc: L('accDesc'), command: 'account', children: [\n { key: 'balance', title: L('accBalance'), desc: '—' },\n // { key: 'status', title: L('accStatus'), desc: '—' },\n { key: 'buy_sub', title: L('accBuySub'), desc: '—' }, // UI-сильно скрыт ниже\n { key: 'buy_credits', title: L('accBuyCreds'), desc: '—' },\n ]},\n\n { key: 'community', title: L('community'), desc: L('communityDesc'), command: 'community' },\n ]\n};\n\n// ---- helpers\nconst BTN_MAIN = L('home');\nconst BTN_BACK = L('back');\nconst BTN_ACCOUNT = L('account'); // web_app button text at bottom (сейчас не используем)\n\nconst hasMedia =\n (Array.isArray(msg.photo) && msg.photo.length>0) ||\n !!msg.document;\n\nconst norm = s => (s||'').normalize('NFKC').toLowerCase()\n .replace(/[\\p{Emoji_Presentation}\\p{Emoji}\\uFE0F]/gu,'')\n .replace(/[\\u0300-\\u036f]/g,'')\n .replace(/[^\\p{L}\\p{N}]+/gu,'').trim();\n\nconst split = p => p ? p.split('/').filter(Boolean) : [];\nfunction nodeByPath(path){\n if(!path) return MENU;\n let n=MENU;\n for(const part of split(path)){\n n=(n.children||[]).find(c=>c.key===part);\n if(!n) return MENU;\n }\n return n;\n}\nfunction parentPath(path){ const a=split(path); a.pop(); return a.join('/'); }\nfunction childKeyByInput(path,txt){\n const want=norm(txt); const n=nodeByPath(path);\n const here=(n.children||[]).find(c=>[c.key,c.title].some(v=>norm(v)===want))?.key;\n if (here) return here;\n const root=(MENU.children||[]).find(c=>[c.key,c.title].some(v=>norm(v)===want))?.key;\n return root||null;\n}\nfunction matchInAccount(txt){\n const acc = (MENU.children||[]).find(c=>c.key==='account');\n if (!acc) return null;\n const want = norm(txt);\n const hit = (acc.children||[]).find(c => [c.key,c.title].some(v => norm(v)===want));\n return hit?.key || null; // 'balance'|'status'|'buy_sub'|'buy_credits'\n}\n\n// ---- nav/mode\nlet path = sd.menu[chatId]?.path || '';\nlet session = sd.session[chatId] || { tool:null };\nlet account_action = null; // <- FLAT actions inside account\n\nconst low = norm(textIn);\n\n// отдельный флаг именно для /start\nconst isStartCmd = /^\\/start\\b/i.test(textIn);\n\n// \"меню\" = /start, /menu и текстовые синонимы\nconst isMenuCmd =\n isStartCmd ||\n /^\\/menu\\b/i.test(textIn) ||\n ['menu','main','home','меню','главная','домой'].includes(low);\n\nconst isBack = [norm(BTN_BACK),'back','назад'].includes(low);\n\n// команды/шорткаты\nconst isBalanceCmd = /^\\/balance\\b/i.test(textIn) || low === 'balance' || low === norm(L('accBalance'));\nconst isBuyCreditsCmd = /^\\/buy_credits\\b/i.test(textIn) || low === 'buycredits' || low === 'buy_credits';\nconst isLangCmd = /^\\/lang\\b/i.test(textIn) || low === 'lang';\n\n// navigation changes\nif (isMenuCmd) {\n path=''; session.tool=null;\n} else if (isBack) {\n const prev=parentPath(path);\n if (split(path).length===1) session.tool=null;\n path=prev;\n} else if (textIn) {\n // direct \"Language\" jump: кнопка «Язык» ИЛИ /lang / lang\n if (norm(textIn) === norm(L('language')) || isLangCmd) {\n path='help/language';\n session.tool='lang';\n } else if (isBalanceCmd) {\n // прямой вызов баланса: /balance или \"Баланс\"\n path = 'account';\n account_action = 'balance';\n session.tool = 'account';\n } else if (isBuyCreditsCmd) {\n // прямой вызов покупки кредитов: /buy_credits\n path = 'account';\n account_action = 'buy_credits';\n session.tool = 'account';\n } else {\n const k = childKeyByInput(path, textIn); // can be child of current or root\n if (k){\n const isRootChild = (MENU.children||[]).some(c=>c.key===k);\n\n if (isRootChild) {\n // go to root child as usual\n path = k;\n const chosen = nodeByPath(path);\n if (chosen?.command) session.tool = chosen.command;\n else session.tool = null;\n } else {\n // child of current node\n if (path === 'account') {\n // FLAT actions: keep path at 'account' and emit action\n account_action = k; // 'balance'|'status'|'buy_sub'|'buy_credits'\n session.tool = 'account';\n // DO NOT change path\n } else {\n // normal deep navigation for other sections\n path = path ? `${path}/${k}` : k;\n const chosen = nodeByPath(path);\n if (chosen?.command) session.tool = chosen.command;\n else if (!chosen?.children?.length) session.tool = null;\n }\n }\n }\n }\n}\n\n// persist\nsd.menu[chatId] = { path, updatedAt: new Date().toISOString(), lang };\nsd.session[chatId] = session;\n\nconst node = nodeByPath(path);\nconst cameFromNav = isMenuCmd || isBack || !!childKeyByInput(parentPath(path), textIn);\nconst isLangTool = session.tool === 'lang';\nconst isAccountAction = session.tool === 'account' && !!account_action;\nconst isInstructionTool = session.tool === 'instruction';\n\n// shouldSendMenu: no menu when lang inline or account action (we'll send specific message)\nconst shouldSendMenu = (isLangTool || isAccountAction || isInstructionTool) ? false : cameFromNav;\nconst isPrompt = !!session.tool && !shouldSendMenu && (textIn || hasMedia);\n\n// ====== Home inline buttons (only on root + когда реально рисуем меню) ======\nlet homeInlineShow = false;\nlet homeInlineText = null;\nlet homeInlineMarkup = null;\n\nif (!path && shouldSendMenu) {\n homeInlineShow = true;\n\n // Текст под инлайн-кнопками\n homeInlineText = (lang === 'ru')\n ? '🚀 Быстрый старт'\n : '🚀 Quick start';\n\n // Пример кнопок — под себя подправишь\n if (lang === 'ru') {\n homeInlineMarkup = {\n inline_keyboard: [\n [\n { text: '⚡️ FLUX', callback_data: 'home:flux' },\n ],\n [\n { text: '📘 Инструкция', callback_data: 'home:instruction' },\n ],\n [\n { text: '💰 Баланс', callback_data: 'account:balance' },\n { text: '⚡️ Купить кредиты', callback_data: 'account:buy_credits' },\n ],\n ],\n };\n } else {\n homeInlineMarkup = {\n inline_keyboard: [\n [\n { text: '⚡️ FLUX', callback_data: 'home:flux' },\n ],\n [\n { text: '📘 Instructions', callback_data: 'home:instruction' },\n ],\n [\n { text: '💰 Balance', callback_data: 'account:balance' },\n { text: '⚡️ Buy credits', callback_data: 'account:buy_credits' },\n ],\n ],\n };\n }\n}\n\n// ====== Keyboard (v1: hide subscription, keep credits) ======\nlet dynNode = node;\n\n// Мягко скрываем пункт \"buy_sub\" в аккаунте (UI), логика ветки buy_sub остаётся\nif (node.key === 'account') {\n dynNode = {\n ...node,\n children: (node.children || []).filter(c => c.key !== 'buy_sub')\n };\n}\n\n// Для community показываем клавиатуру как на главном экране (root MENU),\n// но текст/placeholder остаются от самого раздела community.\nconst keyboardNode = (node.key === 'community') ? MENU : dynNode;\n\n// reply keyboard (skip when lang inline or account action)\nlet replyMarkup = undefined, replyText = undefined;\nif (!isLangTool && !isAccountAction) {\n const rows=[];\n const webapps=(keyboardNode.children||[]).filter(c=>c.type==='webapp');\n for (const c of webapps) {\n rows.push([{ text: c.title, web_app: { url: c.webUrl } }]);\n }\n\n const normals=(keyboardNode.children||[]).filter(c=>c.type!=='webapp');\n for (let i=0; i<normals.length; i+=2) {\n rows.push(normals.slice(i,i+2).map(c => ({ text: c.title })));\n }\n\n const nav = [];\n // На экране community кнопку Back не показываем,\n // чтобы клавиатура была как у home (только Home).\n const showBack = path && node.key !== 'community';\n if (showBack) {\n nav.push({ text: BTN_BACK });\n }\n nav.push({ text: BTN_MAIN });\n rows.push(nav);\n\n\n replyMarkup = {\n keyboard: rows,\n resize_keyboard: true,\n one_time_keyboard: false,\n selective: false,\n input_field_placeholder: dynNode.title || L('placeholder'),\n };\n replyText = (path ? `${dynNode.title}\\n${dynNode.desc||''}` : L('mainTitle')).trim();\n}\n\n\n\n// prompt fields\nconst promptText = msg.caption ?? msg.text ?? '';\n\n// media\nlet photoFileId=null, file_kind=null, file_name=null, mime_type=null;\nconst doc = msg.document;\nif (doc?.file_id) {\n const mt = String(doc.mime_type || '').toLowerCase();\n const name = String(doc.file_name || '').toLowerCase();\n const looksImage = mt.startsWith('image/') || /\\.(png|jpe?g|webp|bmp|gif|tiff?)$/.test(name);\n if (looksImage) { photoFileId=doc.file_id; file_kind='document'; file_name=doc.file_name||null; mime_type=doc.mime_type||null; }\n}\nif (!photoFileId && Array.isArray(msg.photo) && msg.photo.length>0) {\n const best = msg.photo[msg.photo.length-1];\n photoFileId = best.file_id; file_kind='photo';\n}\n\n// output\nreturn [{\n chatId,\n tool: session.tool,\n account_action: account_action || undefined,\n intent: isLangTool ? 'LANG_PICK' : (isAccountAction ? 'ACCOUNT_ACTION' : (isInstructionTool ? 'INSTRUCTION' : undefined)),\n\n shouldSendMenu,\n isPrompt,\n replyText,\n replyMarkup,\n silent: true,\n prompt: promptText,\n\n photo_file_id: photoFileId,\n file_kind, file_name, mime_type,\n\n deleteChatId: chatId,\n deleteMessageId: msgId,\n path,\n lang,\n is_start: isStartCmd,\n\n // NEW:\n homeInlineShow,\n homeInlineText,\n homeInlineMarkup,\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-96,
2016
],
"id": "a2fa5e5b-86d9-46e8-b5e8-946bee127bcc",
"name": "menu"
},
{
"parameters": {
"jsCode": "// build_sub_invoice — подготовка инвойса Stars (XTR) для подписки Standard\n// ВХОД: ожидаем, что сверху пришло tool:'account' и account_action:'buy_sub' + chatId/msg\n// ВЫХОД: invoice (плоские поля для createInvoiceLink), robokassa_url, тексты UI\n\nconst sd = $getWorkflowStaticData('global');\n\n// --- Вход / контекст\nconst chatId = String($json.chatId || $json.message?.chat?.id || '');\nconst msgId = $json.deleteMessageId || $json.message?.message_id || undefined;\n\nconst tool = $json.tool || null;\nconst action = $json.account_action || null;\n\n// Язык: по входу → кэш → подсказка Telegram → ru\nlet lang = (typeof $json.lang === 'string' && /^(ru|en)$/i.test($json.lang)) \n ? $json.lang.toLowerCase()\n : (sd.userLang?.[chatId] || (String($json.message?.from?.language_code || '').startsWith('en') ? 'en' : 'ru'));\nsd.userLang = sd.userLang || {};\nsd.userLang[chatId] = lang;\n\n// Фильтр: запускаемся только на покупку подписки из меню аккаунта\nif (tool !== 'account' || action !== 'buy_sub') {\n return [{ json: { skip: true, reason: 'not_account_buy_sub' } }];\n}\n\n// --- Конфигурация цены (звёзды = XTR, целое число)\nconst PLAN_KEY = 'standard';\nconst PRICE_STARS = Number(sd.pricing?.sub?.standard_stars) || 1; // TODO: поставь свою цену в звёздах\nconst DURATION_DAYS = 30;\n\n// --- Локализация\nconst T = (k) => {\n const RU = {\n title: 'Подписка Стандарт',\n desc: 'Доступ ко всем инструментам на 30 дней',\n label: `Стандарт (${DURATION_DAYS} дней)`,\n prompt: `Выберите способ оплаты подписки «Стандарт» на ${DURATION_DAYS} дней.\\nСтоимость: ${PRICE_STARS} ⭐️`,\n payStars: 'Оплатить звёздами',\n payCard: 'Оплатить картой'\n };\n const EN = {\n title: 'Standard Subscription',\n desc: 'Access to all tools for 30 days',\n label: `Standard (${DURATION_DAYS} days)`,\n prompt: `Choose a payment method for “Standard” ${DURATION_DAYS}-day plan.\\nPrice: ${PRICE_STARS} ⭐️`,\n payStars: 'Pay with Stars',\n payCard: 'Pay by card'\n };\n const dict = (lang === 'en') ? EN : RU;\n return dict[k] || k;\n};\n\n// --- Параметры для createInvoiceLink (Stars/XTR не требует provider_token)\nconst payload = `sub:${PLAN_KEY}:${chatId}:${Date.now()}`;\n\n// Важно: prices должен быть JSON-строкой массива LabeledPrice ({label, amount})\nconst prices = JSON.stringify([{ label: T('label'), amount: PRICE_STARS }]);\n\nconst invoice = {\n title: T('title'),\n description: T('desc'),\n payload,\n currency: 'XTR',\n prices,\n // Не указываем provider_token для Stars!\n // Дополнительно можно добавить:\n // photo_url: 'https://.../preview.jpg',\n // need_name/address/... — НЕ нужно для Stars\n};\n\n// Плейсхолдер для оплаты картой (Robokassa) — просто ссылка на ваш бэкенд\nconst robokassa_url = `https://pay.example/robokassa?plan=${PLAN_KEY}&uid=${encodeURIComponent(chatId)}`;\n\n// Текст сообщения, которое отправим после получения invoice_link\nconst pay_prompt_text = T('prompt');\n\n// Возвращаем все данные для следующих нод\nreturn [{\n json: {\n chatId,\n // Чтобы удалить исходное сообщение пользователя\n deleteChatId: chatId,\n deleteMessageId: msgId,\n\n // Для следующего шага (HTTP Request -> createInvoiceLink)\n invoice, // плоские поля\n // Для сообщения-кнопок\n robokassa_url,\n pay_prompt_text,\n ui_texts: {\n payStars: T('payStars'),\n payCard: T('payCard')\n },\n\n meta: { tool, action, lang, plan: PLAN_KEY, price_stars: PRICE_STARS, duration_days: DURATION_DAYS }\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2032,
432
],
"id": "7bb0d7a4-a8ec-49f1-b251-7e67d34424bf",
"name": "build_sub_invoice",
"disabled": true
},
{
"parameters": {
"jsCode": "// Ловим и парсим pre_checkout_query (в т.ч. Stars: currency === \"XTR\")\n// Если апдейт не тот — ничего не отдаём.\n\nconst pq = $json.pre_checkout_query;\nif (!pq) {\n return [];\n}\n\n// Базовые поля\nconst id = String(pq.id || '');\nconst fromId = Number(pq.from?.id || 0);\nconst username = String(pq.from?.username || '');\nconst chatId = String(pq.chat?.id || pq.from?.id || ''); // для приватных обычно = from.id\nconst currency = String(pq.currency || ''); // XTR для звёзд\nconst total_amount = Number(pq.total_amount || 0);\nconst payload = String(pq.invoice_payload || ''); // то, что вы закладывали при sendInvoice\n\n// Если нужно — валидация payload (план, срок, и т.д.)\n// Здесь просто прокидываем дальше.\nreturn [{\n json: {\n kind: 'pre_checkout',\n chatId,\n userId: fromId,\n username,\n pre_checkout_query_id: id,\n currency,\n total_amount,\n payload,\n // Решение об ок/не ок можно принимать здесь:\n ok: true,\n error_message: null\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-912,
-1280
],
"id": "d320d2ff-a421-415a-afa1-84f967c24d2e",
"name": "payment.precheckout.parse"
},
{
"parameters": {
"method": "POST",
"url": "https://api.telegram.org/bot8495842221:AAHBriXvzudyazB2f8z3ilGQ8A5ESdjjtl8/createInvoiceLink",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "title",
"value": "={{ $json.invoice.title }}"
},
{
"name": "description",
"value": "={{ $json.invoice.description }}"
},
{
"name": "payload",
"value": "={{ $json.invoice.payload }}"
},
{
"name": "currency",
"value": "={{ $json.invoice.currency }}"
},
{
"name": "prices",
"value": "={{ $json.invoice.prices }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2336,
432
],
"id": "4279494d-0a2d-48eb-af5c-ed01d2b3f370",
"name": "createInvoiceLink",
"disabled": true
},
{
"parameters": {
"method": "POST",
"url": "https://api.telegram.org/bot8495842221:AAHBriXvzudyazB2f8z3ilGQ8A5ESdjjtl8/answerPreCheckoutQuery",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "pre_checkout_query_id",
"value": "={{ $json.pre_checkout_query_id }}"
},
{
"name": "ok",
"value": "={{ $json.ok }}"
},
{
"name": "error_message",
"value": "={{ $json.error_message }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-640,
-1280
],
"id": "b50f9952-0532-40f3-ad19-7c47386d768e",
"name": "answerPreCheckoutQuery"
},
{
"parameters": {
"jsCode": "/**\n * Parse Telegram successful_payment → DB payload + ACK\n * Подключение:\n * 1) HTTP → Supabase (используй $json.db_event)\n * 2) Telegram → sendMessage (chat_id = $json.ack.chat_id, text = $json.ack.text)\n */\n\nconst sd = $getWorkflowStaticData('global');\nsd.userLang = sd.userLang || {};\nsd.payments = sd.payments || {}; // идемпотентность по charge_id\n\n// ---- Input\nconst upd = $json || {};\nconst msg = upd.message || {};\nconst sp = msg.successful_payment || {};\n\nconst chatId = String(msg.chat?.id || '');\nconst userId = Number(msg.from?.id || chatId || 0);\nconst uname = String(msg.from?.username || '');\nconst fname = String([msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(' ') || '');\nconst lang = (sd.userLang[chatId] || (/^en/i.test(String(msg.from?.language_code||'')) ? 'en' : 'ru'));\n\nconst currency = String(sp.currency || 'XTR');\nconst amount = Number(sp.total_amount || 0);\nconst payload_raw = String(sp.invoice_payload || '');\nconst tg_charge_id = String(sp.telegram_payment_charge_id || '');\nconst provider_charge_id = String(sp.provider_payment_charge_id || '');\nconst paid_at = new Date().toISOString();\n\n// ---- Safe date utils\nfunction addDays(baseIso, days){\n const base = baseIso ? new Date(baseIso) : new Date();\n if (!Number.isFinite(days)) return null;\n if (isNaN(base.getTime())) return null;\n base.setUTCDate(base.getUTCDate() + days);\n try { return base.toISOString(); } catch { return null; }\n}\nfunction toHuman(iso, lang){\n if (!iso) return '—';\n const d = new Date(iso);\n if (isNaN(d.getTime())) return '—';\n try {\n const loc = lang === 'en' ? 'en' : 'ru';\n return d.toLocaleString(loc, {\n year:'numeric', month:'2-digit', day:'2-digit',\n hour:'2-digit', minute:'2-digit'\n });\n } catch {\n const pad = n => String(n).padStart(2,'0');\n return `${d.getUTCFullYear()}-${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())} UTC`;\n }\n}\n\n// ---- Parse payload (сохраняем nonce, но не используем его для срока)\nfunction parsePayload(raw){\n const out = { raw, type:'unknown', plan:null, credits:null, user:null, extra:{} };\n if (!raw) return out;\n const parts = String(raw).split(':');\n const head = (parts[0]||'').toLowerCase();\n\n if (['sub','subscription'].includes(head)) {\n out.type = 'subscription';\n out.plan = parts[1] || 'standard';\n out.user = Number(parts[2] || userId) || null;\n out.extra.nonce = parts[3] || null; // timestamp/nonce из payload, НЕ срок\n } else if (['credits','cr'].includes(head)) {\n out.type = 'credits';\n const maybe = parts[1] || '';\n const n = Number(maybe);\n out.credits = Number.isFinite(n) && n>0 ? n : (parseInt(String(maybe).replace(/\\D+/g,''),10) || null);\n out.plan = parts[1] || 'pack';\n }\n return out;\n}\nconst pp = parsePayload(payload_raw);\n\n// ---- Idempotency\nlet alreadyProcessed = false;\nif (tg_charge_id) {\n if (sd.payments[tg_charge_id]) alreadyProcessed = true;\n sd.payments[tg_charge_id] = true;\n}\n\n// ---- Business rules (жёстко: подписка = 30 дней и +270 кредитов)\nlet product_kind = pp.type; // 'subscription' | 'credits' | 'unknown'\nlet product_code = pp.plan || 'standard';\nlet term_days = null;\nlet credits_add = pp.credits;\n\nif (product_kind === 'subscription') {\n term_days = 30; // фиксированный срок\n credits_add = 270; // фиксированное начисление\n}\n\n// Ожидаемая дата окончания\nconst expected_until_iso = (product_kind === 'subscription' && term_days)\n ? addDays(paid_at, term_days)\n : null;\n\n// ---- DB payload\nconst db_event = {\n tg_user_id: userId,\n chat_id: chatId,\n username: uname,\n full_name: fname,\n currency,\n amount,\n payload_raw,\n payload_type: product_kind,\n plan: product_code,\n term: null,\n term_days: term_days,\n credits: credits_add,\n lang,\n telegram_payment_charge_id: tg_charge_id,\n provider_payment_charge_id: provider_charge_id,\n paid_at,\n expected_until: expected_until_iso\n};\n\n// ---- ACK text\nlet ackLines = [];\nif (lang === 'en') {\n ackLines.push(alreadyProcessed ? 'ℹ️ Payment was already processed.' : '✅ Payment received.');\n if (product_kind === 'subscription') {\n ackLines.push(`Type: subscription (${product_code})`);\n ackLines.push(`Term: ${term_days} days`);\n ackLines.push(`Credits: +${credits_add}`);\n if (expected_until_iso) ackLines.push(`Valid till (expected): ${toHuman(expected_until_iso,'en')}`);\n } else if (product_kind === 'credits') {\n ackLines.push(`Type: credits`);\n ackLines.push(`Credits: +${credits_add ?? '—'}`);\n } else {\n ackLines.push(`Type: ${product_kind}`);\n }\n ackLines.push(`Amount: ${amount} ${currency}`);\n if (tg_charge_id) ackLines.push(`Receipt: ${tg_charge_id}`);\n} else {\n ackLines.push(alreadyProcessed ? 'ℹ️ Платёж уже был обработан.' : '✅ Оплата получена.');\n if (product_kind === 'subscription') {\n ackLines.push(`Тип: подписка (${product_code})`);\n ackLines.push(`Срок: ${term_days} дн.`);\n ackLines.push(`Кредиты: +${credits_add}`);\n if (expected_until_iso) ackLines.push(`Действует до (ожид.): ${toHuman(expected_until_iso,'ru')}`);\n } else if (product_kind === 'credits') {\n ackLines.push(`Тип: кредиты`);\n ackLines.push(`Кредиты: +${credits_add ?? '—'}`);\n } else {\n ackLines.push(`Тип: ${product_kind}`);\n }\n ackLines.push(`Сумма: ${amount} ${currency}`);\n if (tg_charge_id) ackLines.push(`Чек: ${tg_charge_id}`);\n}\n\nconst ack = {\n chat_id: chatId,\n text: ackLines.join('\\n'),\n disable_notification: true,\n};\n\n// ---- Return\nreturn [{\n json: {\n ok: true,\n kind: 'payment_success',\n chatId,\n userId,\n lang,\n db_event,\n ack\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-912,
-1072
],
"id": "f971aae9-adcd-460e-b37d-5e94e6a8e6f7",
"name": "payment.parse_success"
},
{
"parameters": {
"method": "POST",
"url": "=https://bwbsclwdkighhzlyiman.supabase.co/rest/v1/rpc/apply_telegram_payment",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": " application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"p_tg_user_id\": {{ $json.userId }},\n \"p_provider\": \"telegram-stars\",\n \"p_charge_id\": \"{{ $json.db_event.provider_payment_charge_id }}\",\n \"p_currency\": \"{{ $json.db_event.currency }}\",\n \"p_amount\": {{ $json.db_event.amount }},\n \"p_product_kind\": \"{{ $json.db_event.payload_type || 'unknown' }}\",\n \"p_product_code\": \"{{ $json.db_event.plan || '' }}\",\n \"p_credits_added\": {{ $json.db_event.credits || 0 }},\n \"p_plan\": \"{{ $json.db_event.payload_type === 'subscription'\n ? ($json.db_event.plan || 'standard')\n : null }}\",\n \"p_plan_days\": {{ $json.db_event.payload_type === 'subscription'\n ? ($json.db_event.term_days || 0)\n : 0 }},\n \"p_lang\": \"{{ $json.lang || $json.db_event.lang || 'ru' }}\"\n}\n\n",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-640,
-1072
],
"id": "e9bb008d-dcb9-4b6b-bc08-f67041f48bb1",
"name": "apply_payment",
"credentials": {
"supabaseApi": {
"id": "jGgpXKPYHiL193Rz",
"name": "Supabase account"
}
}
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "58c08747-c6af-4557-9934-fd5bcba58813",
"leftValue": "={{ $json.pre_checkout_query }}",
"rightValue": "",
"operator": {
"type": "object",
"operation": "exists",
"singleValue": true
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d0bf0e9a-81b2-4be3-b897-f8b1c405c5c0",
"leftValue": "={{ $json.message.successful_payment }}",
"rightValue": "",
"operator": {
"type": "object",
"operation": "exists",
"singleValue": true
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.callback_query }}",
"rightValue": "gpt",
"operator": {
"type": "object",
"operation": "exists",
"singleValue": true
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.callback_query }}",
"rightValue": "=flux",
"operator": {
"type": "object",
"operation": "notExists",
"singleValue": true
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
-1328,
1920
],
"id": "d8fd6414-7431-4c53-9815-32a6e54887bf",
"name": "Switch_message_type"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.message.web_app_data }}",
"rightValue": "gpt",
"operator": {
"type": "object",
"operation": "exists",
"singleValue": true
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.message.web_app_data }}",
"rightValue": "=flux",
"operator": {
"type": "object",
"operation": "notExists",
"singleValue": true
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
-320,
2016
],
"id": "878f23f7-8821-4e12-8edd-7034e1b4de40",
"name": "Switch_web_app"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.shouldSendMenu }}",
"rightValue": "gpt",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.shouldSendMenu }}",
"rightValue": "=flux",
"operator": {
"type": "boolean",
"operation": "false",
"singleValue": true
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
400,
2016
],
"id": "c7049219-c441-46c6-a96f-d06cd341662d",
"name": "switch_reply_menu"
},
{
"parameters": {
"jsCode": "// FIRST FUNCTION: parse web_app_data & save settings (FLUX + SEEDANCE + SEEDREAM + VEO + SORA)\nconst sd = $getWorkflowStaticData('global');\nsd.flux = sd.flux || {}; // { [chatId]: { ar, ref, seed, quality, useRef, savedAt } }\nsd.seedance = sd.seedance || {}; // { [chatId]: { mode,tier,model,i2v_mode,resolution,ratio,duration,camerafixed,savedAt } }\nsd.seedream = sd.seedream || {}; // { [chatId]: { quality, ratio, dims_t2i:{w,h}, long_edge_i2i, savedAt } }\nsd.veo = sd.veo || {}; // { [chatId]: { model, ratio, mode, savedAt } }\nsd.sora = sd.sora || {}; // { [chatId]: { model, ratio, duration, savedAt } } <-- NEW\n\nconst msg = $json.message || {};\nconst chatId = String(msg.chat?.id || $json.chat?.id || '');\nconst wad = msg.web_app_data?.data; // JSON-строка из Telegram WebApp\n\nlet saved = false;\nlet kind = null; // 'flux' | 'seedance' | 'seedream' | 'veo' | 'sora'\nlet savedCfg = null;\nlet err = null;\n\nfunction clamp(n, lo, hi) { return Math.min(hi, Math.max(lo, n)); }\n\ntry {\n if (chatId && wad) {\n const raw = JSON.parse(wad);\n\n // ======== FLUX ========\n if (raw?.type === 'flux_settings') {\n kind = 'flux';\n\n const cleanAr = String(raw.ar ?? '1:1').replace(/\\s+/g, '');\n const ar = /^\\d+:\\d+$/.test(cleanAr) ? cleanAr : '1:1';\n\n let ref = Number(raw.ref);\n if (!Number.isFinite(ref)) ref = 0.75;\n ref = clamp(ref, 0, 1.2);\n\n const seed = raw.random ? 0 : (Number(raw.seed) || 0);\n const qNum = Number(raw.quality);\n const bNum = Number(raw.batch);\n const quality = [0,1,2].includes(qNum) ? qNum : 1;\n const batch = [1,2,4].includes(bNum) ? bNum : 1;\n \n const useRef = Boolean(raw.useRef ?? raw.use_ref ?? false);\n\n const cfg = { ar, ref, seed, quality, batch, useRef, savedAt: new Date().toISOString() };\n sd.flux[chatId] = cfg;\n saved = true;\n savedCfg = cfg;\n }\n\n // ======== SEEDANCE ========\n if (raw?.type === 'seedance_settings') {\n kind = 'seedance';\n\n const mode = /^(i2v|t2v)$/.test(String(raw.mode)) ? raw.mode : 'i2v';\n const tier = /^(lite|pro)$/.test(String(raw.tier)) ? raw.tier : 'lite';\n\n const i2v_mode = /^(first|last|first_last|reference)$/.test(String(raw.i2v_mode))\n ? raw.i2v_mode : 'first';\n\n const MODEL_IDS = {\n lite: {\n i2v: 'bytedance-seedance-1-0-lite-i2v-250428',\n t2v: 'bytedance-seedance-1-0-lite-t2v-250428',\n },\n pro: {\n i2v: null,\n t2v: null,\n }\n };\n const fallbackModel = MODEL_IDS?.[tier]?.[mode] || null;\n const model = String(raw.model || fallbackModel || '');\n\n const resolution = /^(480p|720p)$/i.test(String(raw.resolution)) ? raw.resolution.toLowerCase() : '480p';\n\n const R = new Set(['auto','16:9','4:3','1:1','3:4','9:16','21:9']);\n const ratio = R.has(String(raw.ratio)) ? String(raw.ratio) : 'auto';\n\n let duration = Number(raw.duration);\n duration = (duration === 5 || duration === 10) ? duration : 5;\n\n const camerafixed = Boolean(raw.camerafixed);\n\n const cfg = {\n mode, tier, model,\n i2v_mode,\n resolution, ratio, duration, camerafixed,\n savedAt: new Date().toISOString()\n };\n sd.seedance[chatId] = cfg;\n saved = true;\n savedCfg = cfg;\n }\n\n // ======== SEEDREAM (image gen) ========\n if (raw?.type === 'seedream_settings') {\n kind = 'seedream';\n\n let quality = String(raw.quality || '2k').toLowerCase();\n if (!['1k','2k','4k'].includes(quality)) quality = '2k';\n\n const R2 = new Set(['1:1','4:3','3:4','16:9','9:16','3:2','2:3','21:9']);\n let ratio = String(raw.ratio || '1:1');\n if (!R2.has(ratio)) ratio = '1:1';\n\n const BASE_2K = {\n '1:1': { w: 2048, h: 2048 },\n '4:3': { w: 2304, h: 1728 },\n '3:4': { w: 1728, h: 2304 },\n '16:9': { w: 2560, h: 1440 },\n '9:16': { w: 1440, h: 2560 },\n '3:2': { w: 2496, h: 1664 },\n '2:3': { w: 1664, h: 2496 },\n '21:9': { w: 3024, h: 1296 },\n };\n\n const scale = (quality === '1k') ? 0.5 : (quality === '4k') ? 2 : 1;\n const base = BASE_2K[ratio] || BASE_2K['1:1'];\n const dims_t2i = { w: Math.round(base.w * scale), h: Math.round(base.h * scale) };\n const long_edge_i2i = (quality === '1k') ? 1024 : (quality === '4k' ? 4096 : 2048);\n\n const cfg = { quality, ratio, dims_t2i, long_edge_i2i, savedAt: new Date().toISOString() };\n sd.seedream[chatId] = cfg;\n saved = true;\n savedCfg = cfg;\n }\n\n // ======== VEO (video gen) ========\n if (raw?.type === 'veo_settings') {\n kind = 'veo';\n\n let model = String(raw.model || 'veo3.1').toLowerCase();\n if (!/^veo/.test(model)) model = 'veo3.1';\n\n let ratio = String(raw.ratio || '').trim();\n if (!/^(16:9|9:16)$/.test(ratio)) ratio = 'auto';\n\n let mode = String(raw.mode || 't2v');\n if (!/^(t2v|i2v|first_last|reference)$/.test(mode)) mode = 't2v';\n\n const cfg = { model, ratio, mode, savedAt: new Date().toISOString() };\n sd.veo[chatId] = cfg;\n saved = true;\n savedCfg = cfg;\n }\n\n // ======== SORA (video gen) — NEW ========\n if (raw?.type === 'sora_settings') {\n kind = 'sora';\n\n // model: sora-2-pro | sora-2-hd | sora-2\n const ALLOWED = new Set(['sora-2-pro','sora-2-hd','sora-2']);\n let model = String(raw.model || 'sora-2').toLowerCase();\n if (!ALLOWED.has(model)) model = 'sora-2';\n\n // ratio: 16:9 | 9:16\n let ratio = String(raw.ratio || '16:9');\n if (!/^(16:9|9:16)$/.test(ratio)) ratio = '16:9';\n\n // duration: 4 | 8 | 12\n let duration = Number(raw.duration);\n duration = [4,8,12].includes(duration) ? duration : 8;\n\n const cfg = { model, ratio, duration, savedAt: new Date().toISOString() };\n sd.sora[chatId] = cfg;\n saved = true;\n savedCfg = cfg;\n }\n }\n} catch (e) {\n err = String(e?.message || e);\n}\n\nreturn [{\n json: {\n chatId,\n hasWebAppData: Boolean(wad),\n saved,\n kind, // 'flux' | 'seedance' | 'seedream' | 'veo' | 'sora' | null\n savedCfg,\n errorMsg: err || null,\n deleteChatId: chatId,\n deleteMessageId: msg.message_id\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-96,
1808
],
"id": "bfc9c533-bb3d-465c-b2c4-e97efae0a3bf",
"name": "parse web_app_data"
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.lang === 'en' ? '🌐 Choose your language:' : '🌐 Выберите язык:' }}",
"replyMarkup": "inlineKeyboard",
"inlineKeyboard": {
"rows": [
{
"row": {
"buttons": [
{
"text": "Русский ",
"additionalFields": {
"callback_data": "setlang:ru"
}
},
{
"text": "English ",
"additionalFields": {
"callback_data": "setlang:en"
}
}
]
}
}
]
},
"additionalFields": {
"appendAttribution": false
}
},
"id": "42b24f4b-6d0b-4bf9-b679-15699f28dac9",
"name": "lang_inline",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2016,
992
],
"webhookId": "2b9cbcf8-d73a-4d63-ba56-8fac5109a862",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $json.result.chat.id }}",
"messageId": "={{ $('Switch1').item.json.deleteMessageId }}"
},
"id": "d7a6d677-9096-4f04-af9b-6cbf64816edb",
"name": "delMes",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2224,
992
],
"webhookId": "7e67278c-ca08-4b2b-ade0-5fbc6c1e9c14",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $('Switch1').item.json.chatId }}",
"messageId": "={{ $('Switch1').item.json.deleteMessageId }}"
},
"id": "6cff3a8a-d1ce-4931-a54d-84be2051dd0b",
"name": "delMes1",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2576,
176
],
"webhookId": "72e170d1-8ab7-402e-84e2-57ed36c830ef",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $json.chatId }}",
"messageId": "={{ $json.targetMsgId }}"
},
"id": "c5fc35e3-5029-49ff-bdbf-4e40b5ed2c5a",
"name": "delMes_targetMsg",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-448,
-480
],
"webhookId": "263a1d6e-2aab-45a0-ac0a-fd4f61fe227d",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $json.chatId }}",
"messageId": "={{ $json.keyboardMsgId }}"
},
"id": "78fbf151-84d7-4f87-9277-73c25acf66ae",
"name": "delMes_keyboardMsg",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-240,
-480
],
"webhookId": "bfdf4554-b218-4367-b05c-4a3a963e8692",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $('message_reply_inline').item.json.chatId }}",
"messageId": "={{ $('message_reply_inline').item.json.keyboardMsgId }}"
},
"id": "cdd2a43d-31ab-464e-970c-78ffcfbfa81f",
"name": "delMes_keyboardMsg1",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-240,
32
],
"webhookId": "e7190172-8910-40d7-8c25-51328f01d3d9",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "78542b6d-15c2-4809-935c-49d1e17dcc1e",
"leftValue": "={{ $json.account_action }}",
"rightValue": "balance",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "dfbae9da-3cc7-488e-9f71-f21a4b9b4345",
"leftValue": "={{ $json.account_action }}",
"rightValue": "status",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.account_action }}",
"rightValue": "buy_sub",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.account_action }}",
"rightValue": "=buy_credits",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
1760,
304
],
"id": "109d52d5-e6c4-4550-8072-79dd6e29ae30",
"name": "Switch_lang_act"
},
{
"parameters": {
"method": "POST",
"url": "https://api.telegram.org/bot8495842221:AAHBriXvzudyazB2f8z3ilGQ8A5ESdjjtl8/deleteMessage",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "chat_id",
"value": "={{ $('menu').item.json.chatId }}"
},
{
"name": "message_id",
"value": "={{ $('menu').item.json.deleteMessageId }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
816,
1792
],
"id": "21afacf0-2d14-4d4a-a3a3-db692b94eec7",
"name": "deleteMessageMenu",
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const sd = $getWorkflowStaticData('global');\nsd.userLang = sd.userLang || {};\n\n// Инлайн-колбэк от Telegram\nconst cq = $json.callback_query || {};\nconst dataRaw = cq.data;\nconst data = typeof dataRaw === 'string'\n ? dataRaw.trim()\n : String(dataRaw || '').trim();\n\nconst chatId = String(cq.message?.chat?.id || '');\nconst keyboardMsgId = cq.message?.message_id; // наше сообщение с инлайнами\n\nlet action = null; // 'del_one' | 'del_all' | 'setlang' | 'buy_credits_pack'\nlet targetMsgId = null; // для del_one\nlet pack_credits = null; // для buy_credits_pack\nlet lang = sd.userLang[chatId] || null; // 'ru' | 'en', если уже выбран язык\n\n// ---------- разбор callback_data ----------\n// Форматы, которые сейчас используются:\n// - \"del:12345\" → удалить одно сообщение\n// - \"del_all\" → очистить все\n// - \"setlang:ru|en\" → смена языка\n// - \"credits:200\" → выбор пакета кредитов (200/500/1000)\n\nif (data.startsWith('del:')) {\n // Удалить одно сообщение\n action = 'del_one';\n const idStr = data.split(':')[1];\n const n = Number(idStr);\n if (Number.isFinite(n)) targetMsgId = n;\n\n} else if (data === 'del_all') {\n // Удалить все\n action = 'del_all';\n\n} else if (data.startsWith('setlang:')) {\n // Смена языка\n action = 'setlang';\n const tail = data.split(':')[1]?.trim().toLowerCase();\n if (tail === 'ru' || tail === 'en') {\n lang = tail;\n }\n\n} else if (data.startsWith('credits:')) {\n // Выбор пакета кредитов\n // callback_data: \"credits:200\" | \"credits:500\" | \"credits:1000\"\n action = 'buy_credits_pack';\n const tail = data.split(':')[1]?.trim();\n const n = Number(tail);\n if (Number.isFinite(n) && n > 0) {\n pack_credits = n;\n }\n}\n\n// Валидируем язык: по умолчанию 'ru'\nif (!['ru', 'en'].includes(lang)) {\n lang = 'ru';\n}\n\n// Подстраховка для del_one — если id не пришёл в callback_data,\n// попробуем взять из reply_to_message\nif (!targetMsgId && cq.message?.reply_to_message?.message_id) {\n targetMsgId = cq.message.reply_to_message.message_id;\n}\n\nreturn [{\n json: {\n action, // 'del_one' | 'del_all' | 'setlang' | 'buy_credits_pack'\n lang, // язык (особенно важен при setlang)\n chatId,\n targetMsgId, // для del_one\n keyboardMsgId, // id сообщения с инлайн-кнопками\n callback_query_id: cq.id,\n pack_credits // для buy_credits_pack (200/500/1000 или null)\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-912,
-192
],
"id": "849ffd5b-8842-4b3a-83c4-7a6e85965b67",
"name": "message_reply_inline"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.action }}",
"rightValue": "del_one",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.action }}",
"rightValue": "=del_all",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "df33815a-03c8-4a3a-a25f-0a4a7e266f1a",
"leftValue": "={{ $json.action }}",
"rightValue": "setlang",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "fed3019b-9611-47cc-969b-c87ddce299c7",
"leftValue": "={{ $json.action }}",
"rightValue": "buy_credits_pack",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
-704,
-224
],
"id": "1ff60860-9caf-4e9c-8cf7-af9dc606b8f6",
"name": "Switch_inline_act"
},
{
"parameters": {
"chatId": "={{ $('payment.parse_success').item.json.chatId }}",
"text": "={{ $('payment.parse_success').item.json.ack.text }}",
"additionalFields": {
"appendAttribution": false,
"parse_mode": "HTML"
}
},
"id": "5713ca55-a40c-4c5e-a773-37ef474b0840",
"name": "send_mes_payment",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-384,
-1072
],
"webhookId": "c3fe8895-404b-4fa5-a0c8-46733c06592f",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://bwbsclwdkighhzlyiman.supabase.co/rest/v1/rpc/set_user_lang",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": " application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"p_tg_user_id\": {{ $json.chatId }},\n \"p_lang\": \"{{ $json.lang }}\"\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-464,
32
],
"id": "8e6d0bcf-fa72-46fb-bf19-3aadda914ce2",
"name": "SB_set_user_lang",
"credentials": {
"supabaseApi": {
"id": "jGgpXKPYHiL193Rz",
"name": "Supabase account"
}
}
},
{
"parameters": {
"jsCode": "// n8n Code — Build items-to-delete (UNIFIED) — same chatId resolver as \"Clear ALL\"\n// Output: array of { json: { chatId, messageId } } in this order:\n// 1) sd.replyIndex[chatId] (+ keyboardMsgId if provided)\n// 2) sd.photoBucket[chatId].files[].message_id (+ targetMsgId if provided)\n// 3) keys of sd.photoIndex[chatId]\n\nconst sd = $getWorkflowStaticData('global');\n\n// Ensure unified stores (do NOT mutate here)\nsd.photoBucket = sd.photoBucket || {}; // { [chatId]: { files:[{file_id,message_id,...}], savedAt } }\nsd.photoIndex = sd.photoIndex || {}; // { [chatId]: { [messageId]: file_id } }\nsd.replyIndex = sd.replyIndex || {}; // { [chatId]: number[] }\n\n// Same chatId resolution as in your \"Clear ALL\" node\nconst first = $input.first()?.json || {};\nconst msg = $json.message || first.message || {};\nconst chatId = String(\n $json.chatId ||\n first.chatId ||\n msg.chat?.id ||\n $json.chat?.id ||\n first.chat?.id ||\n ''\n);\n\nif (!chatId) {\n return []; // nothing to delete\n}\n\n// Optional context ids from current item\nconst keyboardMsgId = Number($json.keyboardMsgId ?? first.keyboardMsgId ?? 0);\nconst targetMsgId = Number($json.targetMsgId ?? first.targetMsgId ?? 0);\n\n// Helpers\nconst isValidId = (n) => Number.isFinite(n) && n > 0;\nconst uniqOrdered = (arr) => {\n const seen = new Set(); const out = [];\n for (const n of arr) { const v = Number(n); if (isValidId(v) && !seen.has(v)) { seen.add(v); out.push(v); } }\n return out;\n};\n\n// 1) inline replies (replyIndex)\nlet replyIds = Array.isArray(sd.replyIndex[chatId]) ? sd.replyIndex[chatId].slice() : [];\nif (keyboardMsgId) replyIds.push(keyboardMsgId);\nreplyIds = uniqOrdered(replyIds);\n\n// 2) photo message ids from bucket + index\nconst bucketIds = uniqOrdered((sd.photoBucket[chatId]?.files || []).map(x => x?.message_id));\nconst indexIds = uniqOrdered(Object.keys(sd.photoIndex[chatId] || {}));\nlet photoIds = uniqOrdered([...bucketIds, ...indexIds]);\nif (targetMsgId) photoIds = uniqOrdered([...photoIds, targetMsgId]);\n\n// 3) final order: replies first, then photos\nconst all = [...replyIds, ...photoIds];\n\n// Map to items for Telegram deleteMessage\nreturn all.map(messageId => ({ json: { chatId, messageId } }));\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-464,
-208
],
"id": "f02cf1bb-4e6d-44bf-87f6-9f851e828d0b",
"name": "Build items-to-delete"
},
{
"parameters": {
"jsCode": "// Delete ONE image (unified cache)\n// Inputs: chatId (string), targetMsgId (number), keyboardMsgId? (number)\n// Uses: sd.photoBucket, sd.photoIndex, sd.replyIndex\n\nconst sd = $getWorkflowStaticData('global');\nsd.photoBucket = sd.photoBucket || {};\nsd.photoIndex = sd.photoIndex || {};\nsd.replyIndex = sd.replyIndex || {};\n\nconst chatId = String($json.chatId || '');\nconst targetMsgId = Number($json.targetMsgId || 0);\nconst keyboardMsgId = Number($json.keyboardMsgId || 0);\n\nif (!chatId || !Number.isFinite(targetMsgId) || targetMsgId <= 0) {\n return [{ json: { ok:false, reason:'missing_chat_or_target', chatId, targetMsgId } }];\n}\n\n// 1) убрать наш inline-reply из индекса (если есть)\nif (keyboardMsgId && Array.isArray(sd.replyIndex[chatId])) {\n sd.replyIndex[chatId] = sd.replyIndex[chatId].filter(id => id !== keyboardMsgId);\n}\n\n// 2) найти file_id по sd.photoIndex либо по бакету\nconst bucket = sd.photoBucket[chatId];\nconst idxMap = sd.photoIndex[chatId] || {};\nlet removedFileId = idxMap[targetMsgId] || null;\n\nif (!removedFileId && bucket?.files?.length) {\n const hit = bucket.files.find(x => Number(x?.message_id) === targetMsgId);\n if (hit && hit.file_id) removedFileId = hit.file_id;\n}\n\n// 3) удалить из бакета по message_id (и на всякий — по совпадающему file_id)\nlet removed = false;\nif (bucket?.files?.length) {\n const before = bucket.files.length;\n bucket.files = bucket.files.filter(x => {\n if (!x || typeof x !== 'object') return true;\n if (Number(x.message_id) === targetMsgId) return false;\n if (removedFileId && x.file_id === removedFileId) return false;\n return true;\n });\n removed = bucket.files.length < before;\n sd.photoBucket[chatId] = bucket;\n}\n\n// 4) удалить из индекса message_id→file_id (только эту запись)\nif (sd.photoIndex?.[chatId]?.[targetMsgId]) {\n delete sd.photoIndex[chatId][targetMsgId];\n}\n\nreturn [{\n json: {\n ok: true,\n chatId,\n removed,\n removed_file_id: removedFileId || null,\n targetMsgId,\n keyboardMsgId,\n left_in_bucket: sd.photoBucket?.[chatId]?.files?.length || 0\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-48,
-480
],
"id": "068041d9-051b-4218-b482-b6af609ec05b",
"name": "Delete ONE image from memory"
},
{
"parameters": {
"jsCode": "// n8n Code — Clear ALL unified caches for this chat\n// Targets ONLY the unified stores you decided to keep:\n// sd.photoBucket[chatId] — remove\n// sd.photoIndex[chatId] — remove\n// sd.replyIndex[chatId] — remove\n// Leaves sd.poll intact. No legacy/tool-specific caches touched.\n\nconst sd = $getWorkflowStaticData('global');\n\n// Ensure unified stores exist\nsd.photoBucket = sd.photoBucket || {}; // { [chatId]: { files:[{file_id,message_id,...}], savedAt } }\nsd.photoIndex = sd.photoIndex || {}; // { [chatId]: { [messageId]: file_id } }\nsd.replyIndex = sd.replyIndex || {}; // { [chatId]: number[] }\nsd.poll = sd.poll || {};\n\nconst first = $input.first()?.json || {};\nconst msg = $json.message || first.message || {};\nconst chatId = String(\n $json.chatId ||\n first.chatId ||\n msg.chat?.id ||\n $json.chat?.id ||\n first.chat?.id ||\n ''\n);\n\nlet lang = sd.userLang[chatId] || null;\n\nif (!chatId) {\n return [{ json: { ok:false, reason:'no_chat' } }];\n}\n\n// Snapshot BEFORE\nconst before = {\n bucket_files: sd.photoBucket?.[chatId]?.files?.length || 0,\n index_keys: sd.photoIndex?.[chatId] ? Object.keys(sd.photoIndex[chatId]).length : 0,\n reply_len: Array.isArray(sd.replyIndex?.[chatId]) ? sd.replyIndex[chatId].length : 0,\n};\n\n// (optional) collect ids we’re about to drop — handy for downstream debug/logging\nconst bucket_msg_ids = (sd.photoBucket?.[chatId]?.files || [])\n .map(x => Number(x?.message_id))\n .filter(n => Number.isFinite(n) && n > 0);\n\nconst index_msg_ids = sd.photoIndex?.[chatId]\n ? Object.keys(sd.photoIndex[chatId]).map(n => Number(n)).filter(n => Number.isFinite(n) && n > 0)\n : [];\n\nconst reply_msg_ids = Array.isArray(sd.replyIndex?.[chatId]) ? sd.replyIndex[chatId].slice() : [];\n\n// CLEAR unified caches\ndelete sd.photoBucket[chatId];\ndelete sd.photoIndex[chatId];\ndelete sd.replyIndex[chatId];\n\n// Snapshot AFTER\nconst after = {\n bucket_files: sd.photoBucket?.[chatId]?.files?.length || 0,\n index_keys: sd.photoIndex?.[chatId] ? Object.keys(sd.photoIndex[chatId]).length : 0,\n reply_len: Array.isArray(sd.replyIndex?.[chatId]) ? sd.replyIndex[chatId].length : 0,\n};\n\nreturn [{\n json: {\n ok: true,\n lang,\n chatId,\n before,\n after,\n deleted: {\n bucket_msg_ids,\n index_msg_ids,\n reply_msg_ids,\n }\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-240,
-304
],
"id": "0bbabd82-70ab-4f2b-b5b9-e87df2c4f8aa",
"name": "Clear ALL buckets/indexes"
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $json.chatId }}",
"messageId": "={{ $json.messageId }}"
},
"id": "c1ae42f6-0d9f-4aed-b36e-8b10b7ad493d",
"name": "delMes2",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-240,
-144
],
"webhookId": "5becca08-9fa3-416f-8f0c-6e1a114ab9d6",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.lang === 'en'\n ? 'All uploaded photos have been cleared.'\n : 'Все загруженные фото сброшены.'\n}}",
"additionalFields": {
"appendAttribution": false
}
},
"id": "58a2d743-760c-4809-98bc-6f0cfdffc376",
"name": "send_mes_clear",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-48,
-304
],
"webhookId": "a46e0bb1-ac2c-43fc-b456-ef78749654ee",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"chatId": "={{ $('message_reply_inline').item.json.chatId }}",
"text": "={{ $json.lang === 'en'\n ? 'Language saved: English'\n : 'Язык сохранён: Русский'\n }}",
"additionalFields": {
"appendAttribution": false
}
},
"id": "e372a0a1-50a9-4d07-a9a5-113c54b6dd3a",
"name": "send_mes_lang_save",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-48,
32
],
"webhookId": "6f095c52-076a-4679-baf4-721e13505ded",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"chatId": "={{ $json.tg_user_id }}",
"text": "={{ $('Switch_lang_act').item.json.lang === 'en'\n ? 'Current balance: ' + $json.balance + ' credits'\n : 'Текущий баланс вашего аккаунта - ' + $json.balance + ' Кредитов'\n }}",
"additionalFields": {
"appendAttribution": false
}
},
"id": "1585199c-9601-4634-ad56-717376a72cf1",
"name": "send_mes_balance",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2336,
48
],
"webhookId": "92ceebf8-b19e-467b-b2ae-b534a4ea67c3",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.lang === 'en'\n ? 'Current plan: ' + ($json.plan || 'no active plan')\n : 'Текущий статус вашего аккаунта - ' + ($json.plan || 'нет активного плана')\n }}",
"additionalFields": {
"appendAttribution": false
}
},
"id": "8edbccd4-7d68-417c-b90a-c65af0c90abb",
"name": "send_mes_plan",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2336,
256
],
"webhookId": "2a56c513-6002-466f-b3a6-4266d73a69f7",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"chatId": "={{ $('build_sub_invoice').item.json.chatId }}",
"text": "={{ $('build_sub_invoice').item.json.pay_prompt_text }}",
"replyMarkup": "inlineKeyboard",
"inlineKeyboard": {
"rows": [
{
"row": {
"buttons": [
{
"text": "={{ $('build_sub_invoice').item.json.ui_texts.payStars }}",
"additionalFields": {
"url": "={{ $json.result }}"
}
}
]
}
},
{
"row": {
"buttons": [
{
"text": "={{ $('build_sub_invoice').item.json.ui_texts.payCard }}",
"additionalFields": {
"url": "={{ $('build_sub_invoice').item.json.robokassa_url }}"
}
}
]
}
}
]
},
"additionalFields": {
"appendAttribution": false
}
},
"id": "593cdd9f-b109-4b66-ae63-359df315b547",
"name": "send_mes_inline_buy_plan",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2576,
432
],
"webhookId": "e2cbaf9c-00fb-4030-aa3c-6934b8c7a060",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
},
"disabled": true
},
{
"parameters": {
"jsCode": "// n8n Code — Build \"choose credits pack\" message\n// Вход: $json с полями lang, chatId (из меню/роутера)\n// Выход: chatId, lang, text, reply_markup.inline_keyboard\n\nconst msg = $json.message || {};\nconst chatId = String(\n $json.chatId ||\n msg.chat?.id ||\n $json.chat?.id ||\n ''\n);\n\n// fallback, как в menu/аккаунте\nlet lang = (typeof $json.lang === 'string' && $json.lang.toLowerCase() === 'en') ? 'en' : 'ru';\n\n// Определяем тексты\nconst TEXT = {\n ru: {\n title: 'Выберите пакет кредитов:',\n pack: (c) => `${c} Кредитов`,\n },\n en: {\n title: 'Choose a credits pack:',\n pack: (c) => `${c} credits`,\n },\n};\n\nconst T = TEXT[lang] || TEXT.ru;\n\n// Описываем пакеты\nconst packs = [\n { credits: 200 },\n { credits: 500 },\n { credits: 1000 },\n];\n\n// Строим inline_keyboard: по одному пакету в строке\nconst inline_keyboard = packs.map(p => [{\n text: T.pack(p.credits),\n callback_data: `credits:${p.credits}`, // ВАЖНО: формат для Switch_inline_act\n}]);\n\nreturn [{\n json: {\n chatId,\n lang,\n text: T.title,\n reply_markup: {\n inline_keyboard,\n },\n },\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2032,
640
],
"id": "88c3a5a1-7380-49b0-8010-a59a26ce0c95",
"name": "build_credits_packs"
},
{
"parameters": {
"method": "POST",
"url": "https://api.telegram.org/bot8495842221:AAHBriXvzudyazB2f8z3ilGQ8A5ESdjjtl8/sendMessage",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "chat_id",
"value": "={{ $json.chatId }}"
},
{
"name": "text",
"value": "={{ $json.text }}"
},
{
"name": "parse_mode",
"value": "HTML"
},
{
"name": "reply_markup",
"value": "={{ $json.reply_markup }}"
},
{
"name": "disable_notification",
"value": "=true"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2336,
640
],
"id": "80906b35-746c-4a76-ab24-0f859beba4f7",
"name": "reply_markup1",
"retryOnFail": true
},
{
"parameters": {
"method": "POST",
"url": "https://api.telegram.org/bot8495842221:AAHBriXvzudyazB2f8z3ilGQ8A5ESdjjtl8/createInvoiceLink",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.invoicePayload }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-240,
416
],
"id": "96869bd8-cd3e-4b46-8d01-38d35024397a",
"name": "createInvoiceLink_credits"
},
{
"parameters": {
"chatId": "={{ $('build_credits_invoice').item.json.chatId }}",
"text": "={{ $('build_credits_invoice').item.json.ui.caption }}",
"replyMarkup": "inlineKeyboard",
"inlineKeyboard": {
"rows": [
{
"row": {
"buttons": [
{
"text": "={{ $('build_credits_invoice').item.json.ui.btn_stars }}",
"additionalFields": {
"url": "={{ $json.result }}"
}
}
]
}
},
{
"row": {
"buttons": [
{
"text": "={{ $('build_credits_invoice').item.json.ui.btn_card }}",
"additionalFields": {
"url": "={{ $('create_payment_yookassa_credits').item.json.confirmation.confirmation_url }}"
}
}
]
}
}
]
},
"additionalFields": {
"appendAttribution": false,
"parse_mode": "HTML"
}
},
"id": "f9ffaf79-5074-4352-aa5e-d13a34d3b6dd",
"name": "send_mes_payment1",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
192,
512
],
"webhookId": "245ca41a-c6c4-4a94-bb1e-a918af8a165b",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $json.chatId }}",
"messageId": "={{ $json.keyboardMsgId }}"
},
"id": "50b3bb65-e6c0-46d4-b21e-fe33fae4335c",
"name": "delMes_keyboardMsg2",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-464,
832
],
"webhookId": "4dbc69a5-2493-4aac-baca-e4197736aca4",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"jsCode": "// n8n Code — payment_store_prompt\n// Храним последнее \"оплатное\" сообщение с инлайнами в staticData,\n// чтобы потом удалить его после успешной оплаты.\n\nconst sd = $getWorkflowStaticData('global');\nsd.paymentPrompt = sd.paymentPrompt || {}; // { [chatId]: message_id }\n\n// Берём первый входящий item\nconst item = $input.first()?.json || $json || {};\nconst result = item.result || {}; // ответ Telegram sendMessage\nconst chat = result.chat || item.chat || {};\n\nconst chatId = String(\n chat.id ||\n item.chatId ||\n item.db_event?.chat_id ||\n ''\n);\n\nconst messageId = result.message_id;\n\nif (chatId && messageId) {\n sd.paymentPrompt[chatId] = messageId;\n}\n\nreturn [{\n json: {\n chatId,\n messageId,\n stored: !!(chatId && messageId),\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
384,
512
],
"id": "39b6e6e3-f60f-475b-b8fb-3f7e33c15f98",
"name": "payment_store_prompt"
},
{
"parameters": {
"jsCode": "// n8n Code — build_credits_invoice\n// Вход: chatId, lang, pack_credits (200/500/1000)\n// Выход: данные для Telegram Stars + данные для YooKassa (карта)\n\nconst sd = $getWorkflowStaticData('global');\nsd.userLang = sd.userLang || {};\n\nconst chatId = String($json.chatId || '');\nlet lang = (typeof $json.lang === 'string' && $json.lang.toLowerCase() === 'en') ? 'en' : 'ru';\nif (!lang && chatId && sd.userLang[chatId]) lang = sd.userLang[chatId];\n\n// выбранный пак\nconst credits = Number($json.pack_credits || 0);\nif (!Number.isFinite(credits) || credits <= 0) {\n throw new Error('Invalid credits pack selected');\n}\n\n// Прайсинг:\n// amount_stars — цена в Stars (XTR для Telegram)\n// amount_rub — цена в ₽ (для ЮKassa / карты)\nconst pricing = {\n 200: { amount_stars: 159, amount_rub: 199 },\n 500: { amount_stars: 389, amount_rub: 469 },\n 1000:{ amount_stars: 699, amount_rub: 899 },\n};\n\nconst baseCfg = pricing[credits] || pricing[200];\nconst amount_stars = Number(baseCfg.amount_stars) || 0;\nconst amount_rub = Number(baseCfg.amount_rub) || 0;\n\nif (!amount_stars || !amount_rub) {\n throw new Error('Pricing not configured for this credits pack');\n}\n\n// Telegram Stars: минимальные единицы (как «копейки»)\n// Сейчас 1 Star = 1 единица для API (если что — потом поправим unit)\nconst unit = 1;\nconst total_units = amount_stars * unit;\n\n// Локализация текста\nconst T = {\n ru: {\n title: `Пакет ${credits} Кредитов`,\n description: `Покупка ${credits} Кредитов для аккаунта.`,\n // Показываем и Stars, и ₽\n pay_caption:\n `Пакет: ${credits} Кредитов\\n` +\n `Цена звёздами: ${amount_stars} Stars\\n` +\n `Цена по карте: ${amount_rub} ₽`,\n btn_stars: '⭐ Оплатить звёздами',\n btn_card: '💳 Оплатить картой',\n // Для ЮKassa\n yk_description: `Пакет ${credits} кредитов, оплата картой.`,\n },\n en: {\n title: `${credits} Credits pack`,\n description: `Purchase ${credits} credits for your account.`,\n pay_caption:\n `Pack: ${credits} credits\\n` +\n `Price by Stars: ${amount_stars} Stars\\n` +\n `Price by card: ${amount_rub} RUB`,\n btn_stars: '⭐ Pay with Stars',\n btn_card: '💳 Pay by card',\n yk_description: `Pack of ${credits} credits, card payment.`,\n },\n}[lang] || {\n title: `Пакет ${credits} Кредитов`,\n description: `Покупка ${credits} Кредитов для аккаунта.`,\n pay_caption:\n `Пакет: ${credits} Кредитов\\n` +\n `Цена звёздами: ${amount_stars} Stars\\n` +\n `Цена по карте: ${amount_rub} ₽`,\n btn_stars: '⭐ Оплатить звёздами',\n btn_card: '💳 Оплатить картой',\n yk_description: `Пакет ${credits} кредитов, оплата картой.`,\n};\n\n// payload под текущий payment.parse_success:\n// \"credits:200\", \"credits:500\", \"credits:1000\"\nconst payload = `credits:${credits}`;\n\n// === Telegram Stars invoice payload ===\nconst invoicePayload = {\n chat_id: Number(chatId),\n title: T.title,\n description: T.description,\n payload,\n currency: 'XTR',\n prices: [\n { label: T.title, amount: total_units }\n ]\n};\n\n// Пока заглушка под Робокассу (можно будет заменить на реальную ссылку)\nconst robokassa_url = 'https://example.com/robokassa/credits';\n\n// === YooKassa: подготовка данных ===\n\n// product_code — удобно использовать как \"credits_200\" / \"credits_500\" / \"credits_1000\"\nconst yk_product_code = `credits_${credits}`;\n\n// ЮKassa ждёт amount.value строкой с двумя знаками после запятой\nconst yk_amount_value = amount_rub.toFixed(2);\nconst yk_currency = 'RUB';\n\n// Описание платежа для ЮKassa\nconst yk_description = T.yk_description;\n\n// metadata — то, что вернётся в вебхуке (payment.succeeded)\nconst yk_metadata = {\n tg_user_id: chatId,\n credits,\n product_code: yk_product_code,\n source: 'telegram-bot',\n lang,\n};\n\nreturn [{\n json: {\n chatId,\n lang,\n credits,\n\n // Stars\n price_stars: amount_stars,\n\n // Рубли (для карты / ЮKassa)\n price_rub: amount_rub,\n\n // Telegram Stars invoice\n invoicePayload,\n robokassa_url,\n\n // UI для текущих сообщений\n ui: {\n caption: T.pay_caption,\n btn_stars: T.btn_stars,\n btn_card: T.btn_card,\n },\n\n // YooKassa fields\n yk_product_code,\n yk_amount_value,\n yk_currency,\n yk_description,\n yk_metadata,\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-464,
512
],
"id": "ceb2314c-fd38-45fe-9ae5-f4a989e2a37f",
"name": "build_credits_invoice"
},
{
"parameters": {
"jsCode": "// n8n Code — payment_pick_prompt_to_delete\n// Забираем сохранённый message_id инлайна оплаты и готовим данные для deleteMessage.\n\nconst sd = $getWorkflowStaticData('global');\nsd.paymentPrompt = sd.paymentPrompt || {}; // { [chatId]: message_id }\n\n// В этом месте у нас JSON от payment.parse_success / apply_payment\nconst src = $json || {};\nconst chatId = String(\n src.chatId ||\n src.db_event?.chat_id ||\n ''\n);\n\nlet delete_message_id = null;\n\nif (chatId && sd.paymentPrompt[chatId]) {\n delete_message_id = sd.paymentPrompt[chatId];\n // одноразово: после удаления не храним\n delete sd.paymentPrompt[chatId];\n}\n\nreturn [{\n json: {\n ...src,\n delete_chat_id: chatId || null,\n delete_message_id: delete_message_id,\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-640,
-864
],
"id": "94f16d79-e687-470c-8dae-893f6db39790",
"name": "payment_pick_prompt_to_delete"
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $json.chatId }}",
"messageId": "={{ $json.delete_message_id }}"
},
"id": "7efa892e-154f-43c1-b314-27d7710671df",
"name": "delMes3",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
-384,
-864
],
"webhookId": "cb8798c8-d243-4bc9-bf94-f81f77c99c14",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://bwbsclwdkighhzlyiman.supabase.co/rest/v1/rpc/wallet_grant_welcome_once",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": " application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"p_tg_user_id\": \"{{ $('ensure_user').item.json.tg_user_id }}\",\n \"p_tg_username\": \"{{ $('ensure_user').item.json.tg_username }}\",\n \"p_amount\": 10,\n \"p_lang\": \"{{ $json.lang || 'ru' }}\"\n}\n",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
816,
1248
],
"id": "3e1098f2-702c-4209-84a2-d91ba85747c9",
"name": "welcome_once",
"retryOnFail": true,
"credentials": {
"supabaseApi": {
"id": "jGgpXKPYHiL193Rz",
"name": "Supabase account"
}
}
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.is_start }}",
"rightValue": "gpt",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.is_start }}",
"rightValue": "=flux",
"operator": {
"type": "boolean",
"operation": "false",
"singleValue": true
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
112,
2016
],
"id": "d5e4be4b-38b1-49ba-8644-30002f28a152",
"name": "switch_start"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.welcome_applied }}",
"rightValue": "gpt",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.welcome_applied }}",
"rightValue": "=flux",
"operator": {
"type": "boolean",
"operation": "false",
"singleValue": true
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
1024,
1248
],
"id": "74d71777-4e15-4326-b5be-3ea691b0b6a6",
"name": "switch_welcome"
},
{
"parameters": {
"chatId": "={{ $('ensure_user').item.json.tg_user_id }}",
"text": "={{ $('ensure_user').item.json.lang === 'en'\n ? '🎁 We have credited 10 welcome credits to your account.'\n : '🎁 Мы начислили вам 10 приветственных Кредитов.'\n}}",
"additionalFields": {
"appendAttribution": false
}
},
"id": "765d90e2-2da4-4a68-9f89-767f2e1ea456",
"name": "send_wel_cred",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1232,
1248
],
"webhookId": "a2aae37a-1bcb-481d-adcf-d43a7b4b27a6",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"chatId": "={{ $('switch_start').item.json.chatId }}",
"text": "={{ $('switch_start').item.json.lang === 'en'\n ? '🫡 Welcome to GENI AI!'\n : '🫡 Приветствую в GENI AI!'\n}}\n{{ $json.lang === 'en' ? '🌐 Choose your language:' : '🌐 Выберите язык:' }}",
"replyMarkup": "inlineKeyboard",
"inlineKeyboard": {
"rows": [
{
"row": {
"buttons": [
{
"text": "Русский",
"additionalFields": {
"callback_data": "setlang:ru"
}
}
]
}
},
{
"row": {
"buttons": [
{
"text": "English ",
"additionalFields": {
"callback_data": "setlang:en"
}
}
]
}
}
]
},
"additionalFields": {
"appendAttribution": false
}
},
"id": "70f05f2c-cfc0-48e8-8945-9d67c3acdd03",
"name": "send_start_mes",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
816,
1040
],
"webhookId": "1969a35d-d09b-43a2-9df5-891be378c6cf",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"jsCode": "// lang_to_home_after_set\n// Генерируем \"фейковое\" текстовое сообщение `/menu`,\n// чтобы прогнать его через обычный роутинг (Switch_message_type → ensure_user → menu → reply_markup).\n\nconst chatId = String($json.chatId || $input.first().json.result.chat.id);\nconst lang = typeof $('SB_set_user_lang').first().json.lang === 'string' ? $('SB_set_user_lang').first().json.lang.toLowerCase() : null;\n\nif (!chatId) {\n // На всякий случай, если вдруг что-то пошло не так — просто ничего не делаем\n return [];\n}\n\nconst now = Math.floor(Date.now() / 1000);\n\nreturn [{\n json: {\n // Эмулируем update.message, как будто Telegram прислал /menu\n message: {\n message_id: 0, // фиктивный id — он нужен только для совместимости\n from: {\n id: Number(chatId) || chatId,\n is_bot: false,\n language_code: lang || 'ru',\n },\n chat: {\n id: Number(chatId) || chatId,\n type: 'private',\n },\n date: now,\n text: '/menu',\n entities: [\n {\n offset: 0,\n length: 5,\n type: 'bot_command',\n },\n ],\n },\n // Дополнительно можно пробросить lang, но ensure_user уже вернёт актуальный\n lang: lang || undefined,\n },\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
160,
32
],
"id": "5b35d56b-e756-40f6-b8fa-175fea97c464",
"name": "lang_to_home_after_set"
},
{
"parameters": {
"operation": "deleteMessage",
"chatId": "={{ $json.result.chat.id }}",
"messageId": "={{ $('Switch1').item.json.deleteMessageId }}"
},
"id": "bbfccd08-7ba1-426e-ab8f-331276322a8c",
"name": "delMes4",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2224,
1264
],
"webhookId": "5af3ec5f-9501-4891-bb28-8d52e321d7cc",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "// lang_to_home_after_set\n// Генерируем \"фейковое\" текстовое сообщение `/menu`,\n// чтобы прогнать его через обычный роутинг (Switch_message_type → ensure_user → menu → \nconst chatId = String($('Switch1').first().json.chatId);\nconst lang = typeof $('Switch1').first().json.lang === 'string' ? $('Switch1').first().json.lang.toLowerCase() : null;\n\nif (!chatId) {\n // На всякий случай, если вдруг что-то пошло не так — просто ничего не делаем\n return [];\n}\n\nconst now = Math.floor(Date.now() / 1000);\n\nreturn [{\n json: {\n // Эмулируем update.message, как будто Telegram прислал /menu\n message: {\n message_id: 0, // фиктивный id — он нужен только для совместимости\n from: {\n id: Number(chatId) || chatId,\n is_bot: false,\n language_code: lang || 'ru',\n },\n chat: {\n id: Number(chatId) || chatId,\n type: 'private',\n },\n date: now,\n text: '/menu',\n entities: [\n {\n offset: 0,\n length: 5,\n type: 'bot_command',\n },\n ],\n },\n // Дополнительно можно пробросить lang, но ensure_user уже вернёт актуальный\n lang: lang || undefined,\n },\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2464,
1264
],
"id": "98ba3ef7-444e-46e9-b1f2-73d9d293c3bf",
"name": "lang_to_home_after_set1"
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.lang === 'en'\n ? '📘 How to use Geni AI\\n\\n' +\n '1. Open the menu and choose 🖼 Image → ⚡️ FLUX.\\n' +\n '2. Send a text prompt (and optionally a reference photo).\\n' +\n '3. The bot will generate images and spend credits for each request.\\n\\n' +\n '💰 Balance & credits:\\n' +\n '- Use 👤 Account → 💰 Balance to see your credits.\\n' +\n '- Use 👤 Account → ⚡️ Buy credits to top up.\\n\\n' +\n 'If you run out of credits, the bot will show how to buy more.\\n' +\n 'If you need help text to me @GRIGORIY_VOYAKIN.'\n : '📘 Как пользоваться Geni AI\\n\\n' +\n '1. Откройте меню и выберите 🖼 Image → ⚡️ FLUX.\\n' +\n '2. Отправьте текстовый промпт (можно добавить референс-фото).\\n' +\n '3. Бот сгенерирует изображения и спишет Кредиты за запрос.\\n\\n' +\n '💰 Баланс и Кредиты:\\n' +\n '- В разделе 👤 Аккаунт → 💰 Баланс можно посмотреть остаток.\\n' +\n '- В 👤 Аккаунт → ⚡️ Купить кредиты пополнить счёт.\\n\\n' +\n 'Если Кредитов не хватит, бот покажет, как их докупить.\\n' +\n 'Если нужна помощь, напишите мне @GRIGORIY_VOYAKIN.'\n}}",
"additionalFields": {
"appendAttribution": false,
"parse_mode": "HTML"
}
},
"id": "80a83edf-7382-4d9d-8c31-f491521d772b",
"name": "Instruct_mes",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2016,
1264
],
"webhookId": "c063a8aa-5e3d-4f7f-9969-213d4a97cb49",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $('switch_reply_menu').item.json.homeInlineShow }}",
"rightValue": "gpt",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $('switch_reply_menu').item.json.homeInlineShow }}",
"rightValue": "=flux",
"operator": {
"type": "boolean",
"operation": "false",
"singleValue": true
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
624,
1568
],
"id": "450f61d7-17f9-4f05-be64-cadd2feaecf7",
"name": "switch_home_inline",
"disabled": true
},
{
"parameters": {
"method": "POST",
"url": "https://api.telegram.org/bot8495842221:AAHBriXvzudyazB2f8z3ilGQ8A5ESdjjtl8/sendMessage",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "chat_id",
"value": "={{ $('switch_reply_menu').item.json.chatId }}"
},
{
"name": "text",
"value": "={{ $('switch_reply_menu').item.json.homeInlineText }}"
},
{
"name": "parse_mode",
"value": "HTML"
},
{
"name": "reply_markup",
"value": "={{ $('switch_reply_menu').item.json.homeInlineMarkup }}"
},
{
"name": "disable_notification",
"value": "={{ $('switch_reply_menu').item.json.homeInlineShow }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
816,
1568
],
"id": "8133c5ce-d354-46fd-b97f-ee56e2787aa7",
"name": "send_home_inline",
"retryOnFail": true,
"disabled": true
},
{
"parameters": {
"mode": "combine",
"combineBy": "combineByPosition",
"options": {}
},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
-16,
512
],
"id": "8d564289-84e6-4048-9815-87c5901d4763",
"name": "Merge"
},
{
"parameters": {
"method": "POST",
"url": "=https://bwbsclwdkighhzlyiman.supabase.co/rest/v1/payment_messages",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": " application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"provider\": \"yookassa\",\n \"charge_id\": \"{{ $('create_payment_yookassa_credits').item.json.id }}\",\n \"chat_id\": {{ $json.result.chat.id }},\n \"message_id\": {{ $json.result.message_id }}\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
384,
720
],
"id": "d8aa30d1-3f09-4ebe-8189-acff9d636cbe",
"name": "store_payment_message",
"retryOnFail": true,
"credentials": {
"supabaseApi": {
"id": "jGgpXKPYHiL193Rz",
"name": "Supabase account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "https://api.yookassa.ru/v3/payments",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Idempotence-Key",
"value": "={{ $json.chatId + ':' + $json.credits + ':' + Date.now() }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"amount\": {\n \"value\": \"{{ $json.price_rub }}\",\n \"currency\": \"RUB\"\n },\n \"capture\": true,\n \"confirmation\": {\n \"type\": \"redirect\",\n \"return_url\": \"https://t.me/Geni_AI_bot\"\n },\n \"description\": \"{{ `Пакет ${$json.credits} кредитов. TG: ${$json.chatId}` }}\",\n \"metadata\": {\n \"tg_chat_id\": \"{{ $json.chatId }}\",\n \"product\": \"credits\",\n \"credits\": \"{{ $json.credits }}\",\n \"lang\": \"{{ $json.yk_metadata.lang }}\"\n },\n \"receipt\": {\n \"customer\": {\n \"email\": \"grigoriyvoyakinwork@gmail.com\"\n },\n \"items\": [\n {\n \"description\": \"{{ `Пакет ${$json.credits} кредитов` }}\",\n \"quantity\": 1,\n \"amount\": {\n \"value\": \"{{ $json.price_rub }}\",\n \"currency\": \"RUB\"\n },\n \"vat_code\": 1\n }\n ]\n }\n}\n",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-240,
640
],
"id": "200b1c87-9d9b-454c-97a4-043c941bfd85",
"name": "create_payment_yookassa_credits",
"credentials": {
"httpBasicAuth": {
"id": "eoZ95FIg2ZXlecAm",
"name": "yookassa_prod"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://bwbsclwdkighhzlyiman.supabase.co/rest/v1/rpc/get_user_id_and_balance",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": " application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"p_tg_user_id\": \"{{ $json.chatId }}\"\n}\n",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2032,
48
],
"id": "f30d1b6f-58db-44e1-bb7f-1ea561dc249b",
"name": "id_and_balance",
"retryOnFail": true,
"credentials": {
"supabaseApi": {
"id": "jGgpXKPYHiL193Rz",
"name": "Supabase account"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=https://bwbsclwdkighhzlyiman.supabase.co/rest/v1/rpc/ensure_user",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": " application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"p_tg_user_id\": \"{{ $json.chatId }}\",\n \"p_tg_username\": \"{{ $('Telegram Trigger1').item.json.message?.from?.username || '' }}\"\n}\n",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
624,
1184
],
"id": "ff277c5c-10bc-4522-a6f7-63b675ba9e58",
"name": "ensure_user",
"retryOnFail": true,
"credentials": {
"supabaseApi": {
"id": "jGgpXKPYHiL193Rz",
"name": "Supabase account"
}
}
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.action }}",
"rightValue": "STORED",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.action }}",
"rightValue": "=GENERATE",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "9cff2c53-bb3e-49bf-9cfe-53963eac8978",
"leftValue": "={{ $json.action }}",
"rightValue": "CLEARED",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "39ccebaf-cc95-4d0b-954f-71fab6178691",
"leftValue": "={{ $json.action }}",
"rightValue": "NOOP ",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "a9f7ce9b-cd18-4138-83f1-8cb70c4643ed",
"leftValue": "={{ $json.action }}",
"rightValue": "REMIND",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
2032,
2224
],
"id": "d13fd2f7-d37a-42d1-b4bb-b2748182316c",
"name": "Switch2"
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.lang === 'en'\n ? 'Send a photo or a prompt for generation.'\n : 'Пришлите фото или промпт для генерации.'\n}}",
"additionalFields": {
"appendAttribution": false
}
},
"id": "06fdf684-8a54-40e4-b5b3-d2c2e1d2af36",
"name": "Telegram18",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2304,
2704
],
"webhookId": "192bb390-c119-4d8f-b8cc-bbfe51bc0372",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.lang === 'en'\n ? 'All uploaded photos have been cleared.'\n : 'Все загруженные фото сброшены.'\n}}",
"additionalFields": {
"appendAttribution": false
}
},
"id": "deea1c46-34cc-4516-8998-197702f9353f",
"name": "Telegram19",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2304,
2512
],
"webhookId": "3cea3fd1-dd4f-4d78-9b0e-200130d147c1",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.lang === 'en'\n ? `${$json.reply_text}\\n⚡ Generation cost: ${$json.price} credits.\\nTap a button to delete.`\n : `${$json.reply_text}\\n⚡Стоимость генерации: ${$json.price} кредитов.\\nНажмите кнопку, чтобы удалить.`\n}}",
"replyMarkup": "inlineKeyboard",
"inlineKeyboard": {
"rows": [
{
"row": {
"buttons": [
{
"text": "={{ $json.lang === 'en'\n ? '🗑 Delete this photo'\n : '🗑 Удалить это фото'\n}}",
"additionalFields": {
"callback_data": "del:${$json.photo_message_id}"
}
}
]
}
},
{
"row": {
"buttons": [
{
"text": "={{ $json.lang === 'en'\n ? '🧺 Clear all'\n : '🧺 Очистить все'\n}}",
"additionalFields": {
"callback_data": "del_all"
}
}
]
}
}
]
},
"additionalFields": {
"appendAttribution": false,
"reply_to_message_id": "={{ $json.reply_to_message_id }}"
}
},
"id": "72a12cdd-2b6e-41a4-967f-a1799ccbdfe3",
"name": "Telegram20",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2480,
2080
],
"webhookId": "6969c97f-2c91-44e1-ae46-426d11c99897",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
},
{
"parameters": {
"jsCode": "const sd = $getWorkflowStaticData('global');\nsd.photoIndex = sd.photoIndex || {}; // { [chatId]: { [messageId]: file_id } }\n\nconst chatId = String($json.chatId || '');\nconst msgId = Number($json.photo_message_id ?? $json.deleteMessageId ?? 0);\nconst fileId = $json.file_id || $json.photo_file_id || $json.primary_file?.file_id || null;\n\n\nif (chatId && msgId && fileId) {\n sd.photoIndex[chatId] = sd.photoIndex[chatId] || {};\n sd.photoIndex[chatId][msgId] = fileId;\n}\n\nreturn [{ json: $json }];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2304,
2080
],
"id": "6a4b738a-17f0-40fb-b913-75b0cc157b72",
"name": "Code15"
},
{
"parameters": {
"jsCode": "const sd = $getWorkflowStaticData('global');\nsd.replyIndex = sd.replyIndex || {}; // { [chatId]: number[] }\n\nconst chatId = String($json.chatId || $json.result?.chat?.id || '');\nconst sentId =\n Number($json.result?.message_id) ||\n Number($json.result?.message?.message_id) ||\n Number($json.message_id) || 0;\n\nif (chatId && sentId) {\n sd.replyIndex[chatId] = sd.replyIndex[chatId] || [];\n if (!sd.replyIndex[chatId].includes(sentId)) sd.replyIndex[chatId].push(sentId);\n}\n\nreturn [{ json: $json }];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2656,
2080
],
"id": "a0a675a4-7296-4e10-b9fe-cdbba0206956",
"name": "Code23"
},
{
"parameters": {
"jsCode": "// FLUX controller (t2i/i2i) — unified cache only\n// Outputs: action, chatId, reply_text, settings_text, reply_to_message_id,\n// prompt, photo_file_id, ar, useRef, ref, refInterp, seed, quality, lang, balance\n\nconst sd = $getWorkflowStaticData('global');\nsd.flux = sd.flux || {}; // { [chatId]: { ar, ref, seed, quality, useRef } }\nsd.photoBucket = sd.photoBucket || {}; // { [chatId]: { files:[{file_id, message_id, ...}], savedAt } }\nsd.userLang = sd.userLang || {};\n\nconst msg = $json.message || {};\nconst chatId = String($json.chatId || msg.chat?.id || $json.chat?.id || '');\nconst lang = (typeof $json.lang === 'string' && /^(ru|en)$/i.test($json.lang))\n ? $json.lang.toLowerCase()\n : (sd.userLang[chatId] || (/^en/i.test(String(msg.from?.language_code||'')) ? 'en' : 'ru'));\nsd.userLang[chatId] = lang;\n\nconst balance = Number.isFinite(Number($json.balance)) ? Number($json.balance) : null;\nconst emit = (payload) => [{ json: { ...payload, lang, balance } }];\n\nconst promptNorm = typeof $json.prompt === 'string' ? $json.prompt : '';\nconst tgText = typeof msg.text === 'string' ? msg.text : (typeof msg.caption === 'string' ? msg.caption : '');\nconst prompt = (promptNorm || tgText || '').trim();\nconst hasText = prompt.length > 0;\nconst reply_to_message_id = (msg.message_id ?? $json.deleteMessageId ?? null);\n\n// I18N\nconst I18N = {\n ru: {\n stored: (n) => `📥 Фото сохранено (${n}). Отправьте текст — запущу i2i.`,\n remind: `Пришлите текст — запущу t2i, или фото затем текст — запущу i2i.`,\n noop: `Отправьте текст для запуска.`,\n settings: (s) => [\n `🧩 FLUX (auto t2i/i2i)`,\n `📐 AR: ${s.ar} · ${s.dimText}`,\n `🎛 Ref: ${s.useRef ? s.ref.toFixed(2) : 'off'} (interp: ${s.refInterp.toFixed(2)})`,\n `🌱 Seed: ${s.seed === 0 ? 'random' : s.seed}`,\n `⚙️ Quality: ${s.quality === 0 ? 'fast' : 'high'}`\n ].join('\\n'),\n },\n en: {\n stored: (n) => `📥 Photo saved (${n}). Send text to run i2i.`,\n remind: `Send text to run t2i, or photo then text to run i2i.`,\n noop: `Send a text prompt to start.`,\n settings: (s) => [\n `🧩 FLUX (auto t2i/i2i)`,\n `📐 AR: ${s.ar} · ${s.dimText}`,\n `🎛 Ref: ${s.useRef ? s.ref.toFixed(2) : 'off'} (interp: ${s.refInterp.toFixed(2)})`,\n `🌱 Seed: ${s.seed === 0 ? 'random' : s.seed}`,\n `⚙️ Quality: ${s.quality === 0 ? 'fast' : 'high'}`\n ].join('\\n'),\n }\n};\nconst T = I18N[lang] || I18N.ru;\n\n// Flux settings\nconst defaults = { ar: '1:1', ref: 0.75, seed: 0, quality: 1, useRef: false };\nconst stored = (sd.flux[chatId] && typeof sd.flux[chatId] === 'object') ? sd.flux[chatId] : defaults;\n\nlet seedRaw = Number(stored.seed ?? 0);\nif (!Number.isFinite(seedRaw)) seedRaw = 0;\nconst seed = (seedRaw === 0) ? Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) : seedRaw;\n\nlet ref = Number(stored.ref ?? defaults.ref);\nif (!Number.isFinite(ref)) ref = defaults.ref;\nref = Math.max(0, Math.min(1.2, ref));\nconst refInterp = (() => {\n if (!Number.isFinite(ref) || ref <= 0) return 0;\n const t = Math.min(Math.max(ref / 1.2, 0), 1);\n return 5 + (1 - 5) * t;\n})();\n\nlet qRaw = Number(stored.quality ?? defaults.quality);\nconst quality = Number.isFinite(qRaw) ? qRaw : 1;\n\nfunction arToDims(ar) {\n const [w, h] = String(ar || '1:1').split(':').map(n => parseInt(n,10) || 1);\n const base = 1024;\n const W = (w >= h) ? base : Math.round(base * w / h);\n const H = (w >= h) ? Math.round(base * h / w) : base;\n return `${W}×${H}`;\n}\nconst ar = (typeof stored.ar === 'string' && /^\\d+:\\d+$/.test(stored.ar)) ? stored.ar : defaults.ar;\nconst dimText = arToDims(ar);\n\n// detect incoming image\nlet incoming = null;\nif ($json.photo_file_id) {\n incoming = {\n file_id: $json.photo_file_id,\n kind: $json.file_kind || 'photo',\n file_name: $json.file_name || null,\n mime_type: $json.mime_type || null,\n message_id: (msg.message_id ?? $json.deleteMessageId ?? null),\n savedAt: new Date().toISOString()\n };\n}\nif (!incoming && msg.document?.file_id) {\n const mt = String(msg.document.mime_type || '').toLowerCase();\n const name = String(msg.document.file_name || '');\n const looksImage = mt.startsWith('image/') || /\\.(png|jpe?g|webp|bmp|gif|tiff?)$/i.test(name);\n if (looksImage) {\n incoming = {\n file_id: msg.document.file_id,\n kind: 'document',\n file_name: name || null,\n mime_type: msg.document.mime_type || null,\n message_id: (msg.message_id ?? $json.deleteMessageId ?? null),\n savedAt: new Date().toISOString()\n };\n }\n}\nif (!incoming && Array.isArray(msg.photo) && msg.photo.length) {\n const best = msg.photo[msg.photo.length - 1];\n incoming = {\n file_id: best.file_id,\n kind: 'photo',\n file_name: null,\n mime_type: null,\n message_id: (msg.message_id ?? $json.deleteMessageId ?? null),\n savedAt: new Date().toISOString()\n };\n}\n\n// helpers\nfunction ensureBucket() {\n return sd.photoBucket[chatId] ?? { files: [], savedAt: new Date().toISOString() };\n}\nfunction pushLimited(b, f, limit=4) {\n const exists = (b.files || []).some(x => x.file_id === f.file_id);\n if (!exists) {\n (b.files = b.files || []).push(f);\n if (b.files.length > limit) b.files = b.files.slice(-limit);\n }\n return b;\n}\n\n// guards\nif (!chatId) {\n const settings_text = T.settings({ ar, dimText, useRef:false, ref:0, refInterp:0, seed, quality });\n return emit({\n action: 'REMIND', chatId: '',\n reply_text: 'no chat', settings_text, reply_to_message_id: reply_to_message_id ?? null,\n prompt:'', photo_file_id:null, ar, useRef:false, ref:0, refInterp:0, seed, quality\n });\n}\n\n// /clear → очистка бакета\nif (/^\\/clear\\b/i.test(prompt)) {\n if (sd.photoBucket[chatId]?.files) sd.photoBucket[chatId].files = [];\n const settings_text = T.settings({ ar, dimText, useRef:false, ref, refInterp:0, seed, quality });\n return emit({\n action: 'CLEARED',\n chatId,\n reply_text: lang==='en' ? '🗑️ Reference bucket cleared.' : '🗑️ Копилка референсов очищена.',\n settings_text,\n reply_to_message_id,\n prompt: '',\n photo_file_id: null,\n ar, useRef:false, ref:0, refInterp:0, seed, quality\n });\n}\n\n// flow\nconst bucket = sd.photoBucket[chatId] || { files: [] };\nconst files = bucket.files || [];\nconst hasStoredPhoto = files.length > 0;\n\n// 1) Фото без текста → складируем\nif (incoming && !hasText) {\n const b = pushLimited(ensureBucket(), incoming, 4);\n sd.photoBucket[chatId] = b;\n const settings_text = T.settings({ ar, dimText, useRef:true, ref, refInterp, seed, quality });\n return emit({\n action: 'STORED',\n chatId,\n reply_text: T.stored(b.files.length),\n settings_text,\n reply_to_message_id,\n prompt: '',\n photo_file_id: null,\n ar, useRef:true, ref, refInterp, seed, quality\n });\n}\n\n// 2) t2i\nif (hasText && !hasStoredPhoto && !incoming) {\n const settings_text = T.settings({ ar, dimText, useRef:false, ref:0, refInterp:0, seed, quality });\n return emit({\n action: 'GENERATE',\n chatId,\n reply_text: '',\n settings_text,\n reply_to_message_id,\n prompt,\n photo_file_id: null,\n ar, useRef:false, ref:0, refInterp:0, seed, quality\n });\n}\n\n// 3) i2i\nif (hasText && (hasStoredPhoto || incoming)) {\n const all = hasStoredPhoto ? files.slice() : [];\n if (incoming) all.push(incoming);\n const primary = all[all.length - 1];\n\n const settings_text = T.settings({ ar, dimText, useRef:true, ref, refInterp, seed, quality });\n sd.photoBucket[chatId] = { files: all.slice(-4), savedAt: (bucket.savedAt || new Date().toISOString()) };\n\n return emit({\n action: 'GENERATE',\n chatId,\n reply_text: '',\n settings_text,\n reply_to_message_id,\n prompt,\n photo_file_id: primary?.file_id || null,\n ar, useRef:true, ref, refInterp, seed, quality\n });\n}\n\n// 4) подсказки/фолбек\nif (!hasText && !incoming && !hasStoredPhoto) {\n const settings_text = T.settings({ ar, dimText, useRef:false, ref:0, refInterp:0, seed, quality });\n return emit({\n action: 'REMIND',\n chatId,\n reply_text: T.remind,\n settings_text,\n reply_to_message_id,\n prompt: '',\n photo_file_id: null,\n ar, useRef:false, ref:0, refInterp:0, seed, quality\n });\n}\n\nconst settings_text = T.settings({ ar, dimText, useRef:true, ref, refInterp, seed, quality });\nreturn emit({\n action: 'NOOP',\n chatId,\n reply_text: T.noop,\n settings_text,\n reply_to_message_id,\n prompt: '',\n photo_file_id: null,\n ar, useRef:true, ref, refInterp, seed, quality\n});\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1664,
2272
],
"id": "3e318ac2-354b-40dd-a744-cc1318b54142",
"name": "Flux settings"
},
{
"parameters": {
"jsCode": "// n8n Code node — Clear caches after generation (UNIFIED FORMAT)\n// Берёт chatId из ответа Telegram sendPhoto / sendDocument:\n// { ok: true, result: { chat: { id: ... }, ... } }\n//\n// ЧТО ДЕЛАЕТ:\n// - sd.photoBucket[chatId] (CLEARED)\n// - sd.replyIndex[chatId] (CLEARED)\n// - sd.photoIndex[chatId] (PRUNE по message_id; сама map остаётся)\n// $json.deleted_message_ids — по-прежнему опционален.\n\nconst sd = $getWorkflowStaticData('global');\n\n// Ensure unified caches exist\nsd.photoBucket = sd.photoBucket || {}; // { [chatId]: { files:[{file_id, message_id, ...}], savedAt } }\nsd.photoIndex = sd.photoIndex || {}; // { [chatId]: { [messageId]: file_id } }\nsd.replyIndex = sd.replyIndex || {}; // { [chatId]: number[] }\nsd.poll = sd.poll || {}; // left intact\n\n// ---- Определяем chatId из входящего item (ответ Telegram)\nconst item = $input.first()?.json || $json || {};\n\nconst chatId = $('Switch2').first().json.chatId;\n\n// Если вдруг чата нет — ничего не трогаем, но ok=true, чтобы пайплайн не падал\nif (!chatId) {\n return [{\n json: {\n ok: true,\n chatId: null,\n skipped: 'no_chatId_in_input'\n }\n }];\n}\n\n// BEFORE snapshot\nconst before = {\n photoBucket_files: sd.photoBucket?.[chatId]?.files?.length || 0,\n photoIndex_keys: sd.photoIndex?.[chatId] ? Object.keys(sd.photoIndex[chatId]).length : 0,\n replyIndex_len: Array.isArray(sd.replyIndex?.[chatId]) ? sd.replyIndex[chatId].length : 0,\n};\n\n// Собираем message_id для зачистки в photoIndex:\n// 1) из текущего photoBucket[chatId].files\n// 2) опционально — из $json.deleted_message_ids (если кто-то сверху передал)\nconst toPruneSet = new Set();\n\n// from bucket\nconst bucketFiles = sd.photoBucket?.[chatId]?.files || [];\nfor (const x of bucketFiles) {\n const mid = Number(x?.message_id);\n if (Number.isFinite(mid) && mid > 0) toPruneSet.add(String(mid));\n}\n\n// from payload (optional)\nconst extraIds = Array.isArray($json.deleted_message_ids) ? $json.deleted_message_ids : [];\nfor (const mid of extraIds) {\n const n = Number(mid);\n if (Number.isFinite(n) && n > 0) toPruneSet.add(String(n));\n}\n\n// Clear bucket + replyIndex для этого чата\ndelete sd.photoBucket[chatId];\ndelete sd.replyIndex[chatId];\n\n// Prune только нужные ключи из photoIndex (не дропаем карту целиком)\nlet prunedCount = 0;\nif (sd.photoIndex[chatId] && toPruneSet.size > 0) {\n for (const key of toPruneSet) {\n if (Object.prototype.hasOwnProperty.call(sd.photoIndex[chatId], key)) {\n delete sd.photoIndex[chatId][key];\n prunedCount++;\n }\n }\n // Можно было бы удалить sd.photoIndex[chatId], если она опустела,\n // но оставляем, чтобы не дёргать структуру.\n}\n\n// AFTER snapshot\nconst after = {\n photoBucket_files: sd.photoBucket?.[chatId]?.files?.length || 0,\n photoIndex_keys: sd.photoIndex?.[chatId] ? Object.keys(sd.photoIndex[chatId]).length : 0,\n replyIndex_len: Array.isArray(sd.replyIndex?.[chatId]) ? sd.replyIndex[chatId].length : 0,\n};\n\nreturn [{\n json: {\n ok: true,\n chatId,\n pruned_from_photoIndex: prunedCount,\n before,\n after\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2656,
2304
],
"id": "07cb610e-d636-4d67-bbf6-6648a048af39",
"name": "Clean_memory"
},
{
"parameters": {
"jsCode": "// n8n Code — FLUX: price + batch + aspect (w,h)\n\n// Ожидаем, что в $json уже есть:\n// - ar — строка формата \"16:9\", \"3:4\" и т.п. (дефолт \"1:1\")\n// - quality — 0 | 1 | 2 (дефолт 1)\n// - batch / batch_count — на будущее, сейчас из вебаппа не приходит (дефолт 1)\n\nconst arRaw = $json.ar ?? '1:1';\nlet quality = Number($json.quality);\nif (!Number.isFinite(quality) || quality < 0 || quality > 2) {\n quality = 1; // дефолт Standard\n}\n\n// --- Таблица по качеству ---\n// Можно интерпретировать так: \"стоимость одной картинки в кредитах\"\nconst QUALITY_TABLE = {\n 0: { label: 'light', credits: 1.5 },\n 1: { label: 'standard',credits: 2.0 },\n 2: { label: 'pro', credits: 3.0 },\n};\n\n// --- Таблица по батчу (множитель) ---\n// batch = 1 → множитель 1\n// batch = 2 → 1.75\n// batch = 4 → 3\nlet batchCount = Number($json.batch_count ?? $json.batch ?? 1);\nif (!Number.isFinite(batchCount) || ![1, 2, 4].includes(batchCount)) {\n batchCount = 1;\n}\n\nconst BATCH_TABLE = {\n 1: { multiplier: 1.0 },\n 2: { multiplier: 1.75 },\n 4: { multiplier: 3.0 },\n};\n\n// Берём конфиги (с фолбэком на дефолты)\nconst qCfg = QUALITY_TABLE[quality] ?? QUALITY_TABLE[1];\nconst bCfg = BATCH_TABLE[batchCount] ?? BATCH_TABLE[1];\n\n// Итоговая цена в кредитах\nconst creditsCost = qCfg.credits * bCfg.multiplier;\n\n// --- aspect → w,h ---\n// arRaw, например, \"16:9\"\nconst parts = String(arRaw).split(':');\nconst w = parseInt(parts[0], 10) || 1;\nconst h = parseInt(parts[1], 10) || 1;\n\n// Собираем расширенный json\nreturn [{\n json: {\n ...$json,\n quality,\n batch_count: batchCount,\n price: creditsCost,\n w,\n h,\n quality,\n batch_count: batchCount,\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1840,
2272
],
"id": "6377556e-b9a2-41a8-b92e-2add18236be4",
"name": "flux_price_and_aspect"
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "leS7vcfjDNDlHg0o",
"mode": "list",
"cachedResultUrl": "/workflow/leS7vcfjDNDlHg0o",
"cachedResultName": "Geni_AI_FLUX_v002"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {
"waitForSubWorkflow": true
}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
2304,
2304
],
"name": "Call FLUX",
"id": "1919d1ec-14fd-4573-ad60-9a233c235d90"
},
{
"parameters": {
"jsCode": "// Seedream photo-bucket controller (унификация с Seedance)\n// Авто-режим: если есть фото → i2i; иначе → t2i.\n// Настройки читаем из sd.seedream[chatId] (quality 1k/2k/4k, ratio для t2i).\n\nconst sd = $getWorkflowStaticData('global');\nsd.photoBucket = sd.photoBucket || {}; // { [chatId]: { files:[{file_id,...}], savedAt } }\nsd.seedream = sd.seedream || {}; // { [chatId]: { quality, ratio, dims_t2i, long_edge_i2i, savedAt } }\n\n// ---------- inputs (normalized + telegram) ----------\nconst msg = $json.message || {};\nconst chatId = String($json.chatId || msg.chat?.id || $json.chat?.id || '');\nconst tool = $json.tool || 'seedream';\n\nconst normalizedPrompt = typeof $json.prompt === 'string' ? $json.prompt : '';\nconst tgText = typeof msg.text === 'string' ? msg.text : (typeof msg.caption === 'string' ? msg.caption : '');\nconst prompt = (normalizedPrompt || tgText || '').trim();\nconst hasText = prompt.length > 0;\n\n// reply target id (для reply_to_message_id)\nconst reply_to_message_id = (msg.message_id ?? $json.deleteMessageId ?? null);\n\n// ---------- Настройки Seedream ----------\n// Если не сохранены мини-аппом — подставим дефолты и посчитаем размеры\nfunction buildDefaultSeedream() {\n // база 2K (из доки), и масштабируем на 1k/4k\n const BASE_2K = {\n \"1:1\": { w: 2048, h: 2048 },\n \"4:3\": { w: 2304, h: 1728 },\n \"3:4\": { w: 1728, h: 2304 },\n \"16:9\": { w: 2560, h: 1440 },\n \"9:16\": { w: 1440, h: 2560 },\n \"3:2\": { w: 2496, h: 1664 },\n \"2:3\": { w: 1664, h: 2496 },\n \"21:9\": { w: 3024, h: 1296 },\n };\n const SCALES = { '1k':0.5, '2k':1.0, '4k':2.0 };\n const quality = '2k';\n const ratio = '1:1';\n const dims = BASE_2K[ratio];\n return {\n quality,\n ratio,\n dims_t2i: { w: dims.w, h: dims.h },\n long_edge_i2i: 2048, // для i2i\n savedAt: new Date().toISOString()\n };\n}\n\nconst cfg0 = sd.seedream[chatId] || buildDefaultSeedream();\n// валидируем минимально\nconst quality = ['1k','2k','4k'].includes(String(cfg0.quality)) ? cfg0.quality : '2k';\nconst ratio = [\"1:1\",\"4:3\",\"3:4\",\"16:9\",\"9:16\",\"3:2\",\"2:3\",\"21:9\"].includes(String(cfg0.ratio)) ? cfg0.ratio : '1:1';\nconst dims_t2i = (cfg0.dims_t2i && Number(cfg0.dims_t2i.w) && Number(cfg0.dims_t2i.h)) ? cfg0.dims_t2i : null;\nconst long_edge_i2i = Number(cfg0.long_edge_i2i) || (quality==='1k'?1024:(quality==='4k'?4096:2048));\n\nconst cfg = { quality, ratio, dims_t2i, long_edge_i2i, savedAt: cfg0.savedAt };\n\n// формат настройки для текста\nfunction fmtSettingsSeedream(c) {\n const q = c.quality.toUpperCase();\n const ar = c.ratio;\n const dims = c.dims_t2i ? `${c.dims_t2i.w}×${c.dims_t2i.h}` : '—';\n return [\n `🧩 Mode: auto (t2i/i2i)`,\n `🖼 Quality: ${q}`,\n `📐 Aspect (t2i): ${ar} · ${dims}`,\n `↔️ Long edge (i2i): ${c.long_edge_i2i}`\n ].join('\\n');\n}\n\n// ---------- detect incoming image ----------\nlet incoming = null;\n\n// 1) нормализованный photo_file_id\nif ($json.photo_file_id) {\n incoming = {\n file_id: $json.photo_file_id,\n kind: $json.file_kind || 'photo',\n file_name: $json.file_name || null,\n mime_type: $json.mime_type || null,\n message_id: (msg.message_id ?? $json.deleteMessageId ?? null),\n savedAt: new Date().toISOString()\n };\n}\n\n// 2) Telegram document\nif (!incoming && msg.document?.file_id) {\n const mt = String(msg.document.mime_type || '').toLowerCase();\n const name = String(msg.document.file_name || '');\n const looksImage = mt.startsWith('image/') || /\\.(png|jpe?g|webp|bmp|gif|tiff?)$/i.test(name);\n if (looksImage) {\n incoming = {\n file_id: msg.document.file_id,\n kind: 'document',\n file_name: name || null,\n mime_type: msg.document.mime_type || null,\n message_id: (msg.message_id ?? $json.deleteMessageId ?? null),\n savedAt: new Date().toISOString()\n };\n }\n}\n\n// 3) Telegram photo[]\nif (!incoming && Array.isArray(msg.photo) && msg.photo.length) {\n const best = msg.photo[msg.photo.length - 1];\n incoming = {\n file_id: best.file_id,\n kind: 'photo',\n file_name: null,\n mime_type: null,\n message_id: (msg.message_id ?? $json.deleteMessageId ?? null),\n savedAt: new Date().toISOString()\n };\n}\n\n// ---------- guards & commands ----------\nif (!chatId) {\n return [{ json: { action: 'ERROR', reason: 'no_chat', got: Object.keys($json), reply_to_message_id } }];\n}\n\n// /clear очистка корзины\nif (/^\\/clear\\b/i.test(prompt)) {\n delete sd.photoBucket[chatId];\n return [{ json: { action:'CLEARED', chatId, reply:'🗑️ Копилка фото очищена.', settings_seedream: cfg, settings_text: fmtSettingsSeedream(cfg), reply_to_message_id } }];\n}\n\n// ---------- helpers ----------\nfunction ensureBucket() { return sd.photoBucket[chatId] ?? { files: [], savedAt: new Date().toISOString() }; }\nfunction pushLimited(b, f, limit=10) {\n const exists = (b.files || []).some(x => x.file_id === f.file_id);\n if (!exists) {\n (b.files = b.files || []).push(f);\n if (b.files.length > limit) b.files = b.files.slice(-limit);\n }\n return b;\n}\nfunction kbClearBucket() {\n return [[ { text: '🗑 Очистить', callback_data: 'seedream:clear' } ]];\n}\n\n// ====================================================\n// ===================== FLOW =========================\n// ====================================================\n\n// 1) Фото БЕЗ текста → STORED (i2i будет возможен после текста)\nif (incoming && !hasText) {\n const bucket = pushLimited(ensureBucket(), incoming, 10);\n sd.photoBucket[chatId] = bucket;\n return [{\n json: {\n action: 'STORED',\n chatId,\n tool,\n storedCount: bucket.files.length,\n reply: `📥 Фото сохранено (${bucket.files.length}). Теперь отправьте текст — запущу i2i.\\n\\n${fmtSettingsSeedream(cfg)}`,\n inline_keyboard: kbClearBucket(),\n settings_seedream: cfg,\n settings_text: fmtSettingsSeedream(cfg),\n reply_to_message_id\n }\n }];\n}\n\n// 2) Решение по запуску\nconst bucket = sd.photoBucket[chatId] || { files: [] };\nconst files = bucket.files || [];\nconst hasStoredPhoto = files.length > 0;\n\n// Если есть текст и НЕТ фото → t2i\nif (hasText && !hasStoredPhoto && !incoming) {\n // ожидается генерация t2i: из настроек отдаём dims_t2i\n return [{\n json: {\n action: 'GENERATE',\n mode: 't2i',\n chatId,\n tool,\n prompt,\n files: [],\n wants_base64: false,\n clearAfter: false,\n next: 'BUILD_T2I_REQUEST',\n // t2i параметры из настроек (если null — посчитаешь дальше)\n t2i_dims: cfg.dims_t2i || null, // {w,h} или null\n t2i_ratio: cfg.ratio, // '1:1' ...\n t2i_quality: cfg.quality, // '1k'|'2k'|'4k'\n settings_seedream: cfg,\n settings_text: fmtSettingsSeedream(cfg),\n reply_to_message_id\n }\n }];\n}\n\n// /clear очистка корзины\nif (/^\\/clear\\b/i.test(prompt)) {\n delete sd.photoBucket[chatId];\n return [{\n json: {\n action: 'CLEARED',\n chatId,\n reply: '🗑️ Копилка фото очищена.',\n settings_seedream: cfg,\n settings_text: fmtSettingsSeedream(cfg),\n reply_to_message_id\n }\n }];\n}\n\n// Если есть текст и ЕСТЬ фото (или пришло вместе) → i2i\nif (hasText && (hasStoredPhoto || incoming)) {\n const all = hasStoredPhoto ? files.slice() : [];\n if (incoming) all.push(incoming);\n return [{\n json: {\n action: 'GENERATE',\n mode: 'i2i',\n chatId,\n tool,\n prompt,\n files: all, // массив {file_id,...}\n primary_file: all[all.length-1] || null,\n wants_base64: true,\n clearAfter: true, // после старта очищаем корзину\n next: 'FETCH_FILE_BASE64',\n // i2i целевая \"длинная сторона\"\n i2i_long_edge: cfg.long_edge_i2i,\n settings_seedream: cfg,\n settings_text: fmtSettingsSeedream(cfg),\n reply_to_message_id\n }\n }];\n}\n\n// Если текста нет и фото нет → подсказка\nif (!hasText && !incoming && !hasStoredPhoto) {\n return [{\n json: {\n action: 'REMIND',\n chatId,\n tool,\n reply: `Пришлите текст — запущу t2i, или пришлите фото, затем текст — запущу i2i.\\n\\n${fmtSettingsSeedream(cfg)}`,\n settings_seedream: cfg,\n settings_text: fmtSettingsSeedream(cfg),\n reply_to_message_id\n }\n }];\n}\n\n// Фолбек (например, пришло фото и ещё фото без текста)\nreturn [{\n json: {\n action: 'NOOP',\n chatId,\n tool,\n reply: `Отправьте текст для запуска.\\n\\n${fmtSettingsSeedream(cfg)}`,\n settings_seedream: cfg,\n settings_text: fmtSettingsSeedream(cfg),\n reply_to_message_id\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1664,
3072
],
"id": "ef8f52dd-7dba-451a-a8e3-12cff8aa7384",
"name": "Seedream — photo store & launch"
},
{
"parameters": {
"jsCode": "// Seedream — builder for Comet /v1/images/generations (t2i: WIDTHxHEIGHT, i2i: 1k/2k/4k)\n\nconst MODEL = 'bytedance-seedream-4-0-250828';\n\n// 1) Входы\nconst cfg = $json.settings_seedream || {};\nconst prompt = String($json.prompt || '').trim();\nconst n = Number.isFinite(Number($json.n)) ? Number($json.n) : 1;\n\n// helpers\nfunction isDataUrl(u){ return typeof u === 'string' && /^data:image\\/[a-z0-9.+-]+;base64,/i.test(u); }\nfunction isHttp(u){ return typeof u === 'string' && /^https?:\\/\\//i.test(u); }\nfunction extractImages(src){\n const out = [];\n if (!src) return out;\n if (Array.isArray(src)) {\n for (const it of src) {\n if (typeof it === 'string' && (isDataUrl(it) || isHttp(it))) out.push(it);\n else if (it && typeof it === 'object') {\n const cand = it.dataUrl || it.url || it.data || it.image_url || it.imageUrl;\n if (typeof cand === 'string' && (isDataUrl(cand) || isHttp(cand))) out.push(cand);\n }\n }\n } else if (typeof src === 'string' && (isDataUrl(src) || isHttp(src))) {\n out.push(src);\n } else if (src && typeof src === 'object') {\n const cand = src.dataUrl || src.url || src.data || src.image_url || src.imageUrl;\n if (typeof cand === 'string' && (isDataUrl(cand) || isHttp(cand))) out.push(cand);\n }\n return out;\n}\n\n// 2) Соберём изображения (если есть — это i2i)\nlet images = [];\nimages = images.concat(extractImages($json.dataUrls));\nimages = images.concat(extractImages($json.images));\nimages = images.concat(extractImages($json.dataUrl));\nimages = images.concat(extractImages($json.image_url));\nimages = Array.from(new Set(images));\n\nconst isI2I = images.length > 0;\n\n// 3) Определяем size\nlet sizeValue;\n\n// t2i → WIDTHxHEIGHT\nif (!isI2I) {\n const dims = $json.t2i_dims || cfg.dims_t2i || null;\n let w = Number(dims?.w), h = Number(dims?.h);\n if (!(Number.isFinite(w) && Number.isFinite(h) && w>0 && h>0)) {\n // fallback по качеству/соотношению, если вдруг dims не пришли\n // по умолчанию 2k квадрат\n const q = (cfg.quality || '2k').toLowerCase();\n if (q === '1k') { w = 1024; h = 1024; }\n else if (q === '4k') { w = 4096; h = 4096; }\n else { w = 2048; h = 2048; }\n }\n sizeValue = `${Math.round(w)}x${Math.round(h)}`;\n} else {\n // i2i → '1k'|'2k'|'4k' (как раньше)\n let size = (cfg.quality || '2k').toLowerCase();\n if (!['1k','2k','4k'].includes(size)) size = '2k';\n sizeValue = size;\n}\n\n// 4) Сборка payload\nconst body = {\n model: MODEL,\n prompt: prompt,\n n: n,\n size: sizeValue,\n response_format: 'url',\n watermark: false\n};\nif (isI2I) body.image = images;\n\n// 5) Выход\nreturn [{\n json: {\n method: 'POST',\n url: 'https://api.cometapi.com/v1/images/generations',\n headers: { 'Content-Type': 'application/json' },\n body\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2384,
3440
],
"id": "3ef7d833-7c59-4a20-b67b-8461aa027bf7",
"name": "Нормализовать task_id1",
"alwaysOutputData": false
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6337ea59-4635-406e-89d4-62505dae4f35",
"leftValue": "={{ $json.action }}",
"rightValue": "STORED",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "d6fc6a81-7010-4119-9d91-c66d79e68ac8",
"leftValue": "={{ $json.action }}",
"rightValue": "=GENERATE",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "9cff2c53-bb3e-49bf-9cfe-53963eac8978",
"leftValue": "={{ $json.action }}",
"rightValue": "CLEARED",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "39ccebaf-cc95-4d0b-954f-71fab6178691",
"leftValue": "={{ $json.action }}",
"rightValue": "NOOP ",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "a9f7ce9b-cd18-4138-83f1-8cb70c4643ed",
"leftValue": "={{ $json.action }}",
"rightValue": "REMIND",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
2032,
3024
],
"id": "8f1d2ee7-ebaa-44be-996b-958adf1fbb1f",
"name": "Switch"
},
{
"parameters": {
"jsCode": "const sd = $getWorkflowStaticData('global');\nsd.photoIndex = sd.photoIndex || {}; // { [chatId]: { [messageId]: file_id } }\n\nconst chatId = String($json.chatId || '');\nconst msgId = Number($json.photo_message_id ?? $json.deleteMessageId ?? 0);\nconst fileId = $json.file_id || $json.photo_file_id || $json.primary_file?.file_id || null;\n\n\nif (chatId && msgId && fileId) {\n sd.photoIndex[chatId] = sd.photoIndex[chatId] || {};\n sd.photoIndex[chatId][msgId] = fileId;\n}\n\nreturn [{ json: $json }];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2336,
2976
],
"id": "4792537f-3ed4-4206-8839-bab0460b5cdf",
"name": "Code"
},
{
"parameters": {
"jsCode": "const sd = $getWorkflowStaticData('global');\nsd.replyIndex = sd.replyIndex || {}; // { [chatId]: number[] }\n\nconst chatId = String($json.chatId || $json.result?.chat?.id || '');\nconst sentId =\n Number($json.result?.message_id) ||\n Number($json.result?.message?.message_id) ||\n Number($json.message_id) || 0;\n\nif (chatId && sentId) {\n sd.replyIndex[chatId] = sd.replyIndex[chatId] || [];\n if (!sd.replyIndex[chatId].includes(sentId)) sd.replyIndex[chatId].push(sentId);\n}\n\nreturn [{ json: $json }];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2672,
2976
],
"id": "c566fc05-b832-407f-91a0-e1b2570df49c",
"name": "Code24"
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.lang === 'en'\n ? `${$json.reply}\\n⚡ Generation cost: ${$json.price} credits.\\nTap a button to delete.`\n : `${$json.reply}\\n⚡Стоимость генерации: ${$json.price} кредитов.\\nНажмите кнопку, чтобы удалить.`\n}}",
"replyMarkup": "inlineKeyboard",
"inlineKeyboard": {
"rows": [
{
"row": {
"buttons": [
{
"text": "={{ $json.lang === 'en'\n ? '🗑 Delete this photo'\n : '🗑 Удалить это фото'\n}}",
"additionalFields": {
"callback_data": "del:${$json.photo_message_id}"
}
}
]
}
},
{
"row": {
"buttons": [
{
"text": "={{ $json.lang === 'en'\n ? '🧺 Clear all'\n : '🧺 Очистить все'\n}}",
"additionalFields": {
"callback_data": "del_all"
}
}
]
}
}
]
},
"additionalFields": {
"appendAttribution": false,
"reply_to_message_id": "={{ $json.reply_to_message_id }}"
}
},
"id": "8cc27531-b8e6-4985-a60b-0826c028b6f9",
"name": "Telegram",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2496,
2976
],
"webhookId": "6969c97f-2c91-44e1-ae46-426d11c99897",
"credentials": {
"telegramApi": {
"id": "ur7jSUPdiAaPVhCf",
"name": "Geni AI"
}
}
}
],
"connections": {
"Switch1": {
"main": [
[
{
"node": "Switch_lang_act",
"type": "main",
"index": 0
}
],
[
{
"node": "lang_inline",
"type": "main",
"index": 0
}
],
[
{
"node": "Instruct_mes",
"type": "main",
"index": 0
}
],
[
{
"node": "Seedream — photo store & launch",
"type": "main",
"index": 0
}
]
]
},
"Telegram Trigger1": {
"main": [
[
{
"node": "Switch_message_type",
"type": "main",
"index": 0
}
]
]
},
"reply_markup": {
"main": [
[
{
"node": "deleteMessageMenu",
"type": "main",
"index": 0
}
]
]
},
"menu": {
"main": [
[
{
"node": "switch_start",
"type": "main",
"index": 0
}
]
]
},
"build_sub_invoice": {
"main": [
[
{
"node": "createInvoiceLink",
"type": "main",
"index": 0
}
]
]
},
"payment.precheckout.parse": {
"main": [
[
{
"node": "answerPreCheckoutQuery",
"type": "main",
"index": 0
}
]
]
},
"createInvoiceLink": {
"main": [
[
{
"node": "send_mes_inline_buy_plan",
"type": "main",
"index": 0
},
{
"node": "delMes1",
"type": "main",
"index": 0
}
]
]
},
"answerPreCheckoutQuery": {
"main": [
[]
]
},
"payment.parse_success": {
"main": [
[
{
"node": "apply_payment",
"type": "main",
"index": 0
},
{
"node": "payment_pick_prompt_to_delete",
"type": "main",
"index": 0
}
]
]
},
"apply_payment": {
"main": [
[
{
"node": "send_mes_payment",
"type": "main",
"index": 0
}
]
]
},
"Switch_message_type": {
"main": [
[
{
"node": "payment.precheckout.parse",
"type": "main",
"index": 0
}
],
[
{
"node": "payment.parse_success",
"type": "main",
"index": 0
}
],
[
{
"node": "message_reply_inline",
"type": "main",
"index": 0
}
],
[
{
"node": "Switch_web_app",
"type": "main",
"index": 0
}
]
]
},
"Switch_web_app": {
"main": [
[
{
"node": "parse web_app_data",
"type": "main",
"index": 0
}
],
[
{
"node": "menu",
"type": "main",
"index": 0
}
]
]
},
"switch_reply_menu": {
"main": [
[
{
"node": "reply_markup",
"type": "main",
"index": 0
}
],
[
{
"node": "Switch1",
"type": "main",
"index": 0
}
]
]
},
"parse web_app_data": {
"main": [
[]
]
},
"lang_inline": {
"main": [
[
{
"node": "delMes",
"type": "main",
"index": 0
}
]
]
},
"delMes_keyboardMsg1": {
"main": [
[]
]
},
"Switch_lang_act": {
"main": [
[
{
"node": "id_and_balance",
"type": "main",
"index": 0
}
],
[
{
"node": "send_mes_plan",
"type": "main",
"index": 0
}
],
[
{
"node": "build_sub_invoice",
"type": "main",
"index": 0
}
],
[
{
"node": "build_credits_packs",
"type": "main",
"index": 0
}
]
]
},
"message_reply_inline": {
"main": [
[
{
"node": "Switch_inline_act",
"type": "main",
"index": 0
}
]
]
},
"Switch_inline_act": {
"main": [
[
{
"node": "delMes_targetMsg",
"type": "main",
"index": 0
},
{
"node": "delMes_keyboardMsg",
"type": "main",
"index": 0
},
{
"node": "Delete ONE image from memory",
"type": "main",
"index": 0
}
],
[
{
"node": "Build items-to-delete",
"type": "main",
"index": 0
}
],
[
{
"node": "SB_set_user_lang",
"type": "main",
"index": 0
}
],
[
{
"node": "build_credits_invoice",
"type": "main",
"index": 0
},
{
"node": "delMes_keyboardMsg2",
"type": "main",
"index": 0
}
]
]
},
"SB_set_user_lang": {
"main": [
[
{
"node": "delMes_keyboardMsg1",
"type": "main",
"index": 0
},
{
"node": "send_mes_lang_save",
"type": "main",
"index": 0
}
]
]
},
"Build items-to-delete": {
"main": [
[
{
"node": "delMes2",
"type": "main",
"index": 0
},
{
"node": "Clear ALL buckets/indexes",
"type": "main",
"index": 0
}
]
]
},
"send_mes_balance": {
"main": [
[
{
"node": "delMes1",
"type": "main",
"index": 0
}
]
]
},
"send_mes_plan": {
"main": [
[
{
"node": "delMes1",
"type": "main",
"index": 0
}
]
]
},
"Clear ALL buckets/indexes": {
"main": [
[
{
"node": "send_mes_clear",
"type": "main",
"index": 0
}
]
]
},
"build_credits_packs": {
"main": [
[
{
"node": "reply_markup1",
"type": "main",
"index": 0
}
]
]
},
"createInvoiceLink_credits": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"reply_markup1": {
"main": [
[
{
"node": "delMes1",
"type": "main",
"index": 0
}
]
]
},
"build_credits_invoice": {
"main": [
[
{
"node": "createInvoiceLink_credits",
"type": "main",
"index": 0
},
{
"node": "create_payment_yookassa_credits",
"type": "main",
"index": 0
}
]
]
},
"send_mes_payment1": {
"main": [
[
{
"node": "payment_store_prompt",
"type": "main",
"index": 0
},
{
"node": "store_payment_message",
"type": "main",
"index": 0
}
]
]
},
"payment_pick_prompt_to_delete": {
"main": [
[
{
"node": "delMes3",
"type": "main",
"index": 0
}
]
]
},
"welcome_once": {
"main": [
[
{
"node": "switch_welcome",
"type": "main",
"index": 0
}
]
]
},
"switch_start": {
"main": [
[
{
"node": "ensure_user",
"type": "main",
"index": 0
}
],
[
{
"node": "switch_reply_menu",
"type": "main",
"index": 0
}
]
]
},
"switch_welcome": {
"main": [
[
{
"node": "send_wel_cred",
"type": "main",
"index": 0
}
]
]
},
"send_mes_lang_save": {
"main": [
[
{
"node": "lang_to_home_after_set",
"type": "main",
"index": 0
}
]
]
},
"lang_to_home_after_set": {
"main": [
[
{
"node": "Switch_message_type",
"type": "main",
"index": 0
}
]
]
},
"delMes": {
"main": [
[]
]
},
"delMes4": {
"main": [
[
{
"node": "lang_to_home_after_set1",
"type": "main",
"index": 0
}
]
]
},
"lang_to_home_after_set1": {
"main": [
[
{
"node": "Switch_message_type",
"type": "main",
"index": 0
}
]
]
},
"Instruct_mes": {
"main": [
[
{
"node": "delMes4",
"type": "main",
"index": 0
}
]
]
},
"switch_home_inline": {
"main": [
[
{
"node": "send_home_inline",
"type": "main",
"index": 0
}
]
]
},
"Merge": {
"main": [
[
{
"node": "send_mes_payment1",
"type": "main",
"index": 0
}
]
]
},
"create_payment_yookassa_credits": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"id_and_balance": {
"main": [
[
{
"node": "send_mes_balance",
"type": "main",
"index": 0
}
]
]
},
"ensure_user": {
"main": [
[
{
"node": "welcome_once",
"type": "main",
"index": 0
},
{
"node": "send_start_mes",
"type": "main",
"index": 0
}
]
]
},
"Switch2": {
"main": [
[
{
"node": "Code15",
"type": "main",
"index": 0
}
],
[
{
"node": "Call FLUX",
"type": "main",
"index": 0
}
],
[
{
"node": "Telegram19",
"type": "main",
"index": 0
}
],
[
{
"node": "Telegram18",
"type": "main",
"index": 0
}
],
[
{
"node": "Telegram18",
"type": "main",
"index": 0
}
]
]
},
"Telegram20": {
"main": [
[
{
"node": "Code23",
"type": "main",
"index": 0
}
]
]
},
"Code15": {
"main": [
[
{
"node": "Telegram20",
"type": "main",
"index": 0
}
]
]
},
"Flux settings": {
"main": [
[
{
"node": "flux_price_and_aspect",
"type": "main",
"index": 0
}
]
]
},
"flux_price_and_aspect": {
"main": [
[
{
"node": "Switch2",
"type": "main",
"index": 0
}
]
]
},
"Call FLUX": {
"main": [
[
{
"node": "Clean_memory",
"type": "main",
"index": 0
}
]
]
},
"Seedream — photo store & launch": {
"main": [
[
{
"node": "Switch",
"type": "main",
"index": 0
}
]
]
},
"Switch": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"Code": {
"main": [
[
{
"node": "Telegram",
"type": "main",
"index": 0
}
]
]
},
"Telegram": {
"main": [
[
{
"node": "Code24",
"type": "main",
"index": 0
}
]
]
}
},
"authors": "Grigoriy Voyakin",
"name": null,
"description": null
}
}