{
 "id": "R1zn1l8zDaVpFSkm",
 "name": "OOeirense - Telegram → Instagram (HTML)",
 "active": 1,
 "nodes": [
  {
   "parameters": {
    "jsCode": "const response = $input.first().json.body || $input.first().json;\nconst results = response.result || [];\nif (!results.length) { return []; }\nconst msg = results[0];\nconst updateId = msg.update_id;\nconst newOffset = updateId + 1;\nconst staticData = $getWorkflowStaticData('global');\nstaticData.lastUpdateId = newOffset;\nif (msg.message?.from?.is_bot) { return []; }\nconst chatId = String(msg.message?.chat?.id || '');\nif (chatId !== '7769073261') { return []; }\nconst text = msg.message?.text || '';\nconst trimmed = text.trim();\nconst instaMatch = trimmed.match(/^insta\\s+(https?:\\/\\/[^\\s<>\"'\\)]+)/i);\nif (!instaMatch) { return []; }\nconst url = instaMatch[1];\n\n// DEDUP À PROVA DE RACE — lockfile filesystem via mkdir atômico\nconst { execFileSync } = require('child_process');\nconst LOCK_DIR = '/tmp/ooeirense-seen';\nconst TTL_MIN = 360; // 6h\ntry { execFileSync('/bin/mkdir', ['-p', LOCK_DIR]); } catch(e) {}\n\n// Limpa locks antigos (best-effort, não bloqueia)\ntry {\n  execFileSync('/usr/bin/find', [LOCK_DIR, '-maxdepth', '1', '-mindepth', '1', '-type', 'd', '-mmin', '+' + TTL_MIN, '-exec', '/bin/rm', '-rf', '{}', '+'], { timeout: 2000 });\n} catch(e) {}\n\n// Tenta reivindicar o update_id — mkdir sem -p falha se já existe (atômico no fs)\nconst lockPath = LOCK_DIR + '/u' + updateId;\nlet acquired = false;\ntry {\n  execFileSync('/bin/mkdir', [lockPath]);\n  acquired = true;\n} catch(e) {\n  acquired = false;\n}\nif (!acquired) {\n  return [{ json: { url, messageId: msg.message.message_id, updateId, newOffset, duplicate: true, reason: 'concurrent-lock' } }];\n}\n\nreturn [{ json: { url, messageId: msg.message.message_id, updateId, newOffset, duplicate: false } }];\n"
   },
   "id": "tg-extract-url",
   "name": "Extrair URL do Telegram",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    688,
    304
   ]
  },
  {
   "parameters": {
    "url": "={{ 'https://r.jina.ai/' + $json.url }}",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Accept",
       "value": "application/json"
      },
      {
       "name": "User-Agent",
       "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
      }
     ]
    },
    "options": {}
   },
   "id": "ce8500de-951a-4cfb-b329-4b16e21e0718",
   "name": "Buscar Matéria",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    1408,
    304
   ]
  },
  {
   "parameters": {
    "jsCode": "// Jina Reader retorna: { data: { title, description, url, content, metadata } }\nconst response = $input.first().json;\nconst jinaData = response.data || response;\nconst meta = jinaData.metadata || {};\n\n// Jina devolve ARRAY quando a pagina repete a mesma meta tag (dois plugins de SEO\n// no mesmo WordPress). Ex: portalintegracao.com.br duplica og:title/og:description/og:image.\n// Sem normalizar, .replace/.trim quebram com \"is not a function\".\nconst txt = (v) => {\n  if (Array.isArray(v)) v = v.find(x => typeof x === 'string' && x.trim());\n  return typeof v === 'string' ? v : '';\n};\n\nconst articleUrl = $('Extrair URL do Telegram').first().json.url;\nconst emailId = $('Extrair URL do Telegram').first().json.emailId;\n\n// ---------------------------------------------------------------------------\n// GUARDA 1: a URL ainda existe? (25/08/2026)\n// Editar o titulo de uma materia no painel do portal regera o slug, e o link\n// antigo vira 404. O Jina responde 200 servindo a PAGINA DE ERRO, que tem\n// og:image (og-home.jpg) e corpo (menu + ultimas noticias): passava por todos\n// os testes daqui e o bot publicava o 404 como se fosse materia (2 posts em\n// 25/08/2026, execucoes 595209 e 595298). Confere o status HTTP na fonte.\n// ATENCAO ao texto dos throw daqui: o n8n corta a mensagem de erro no ultimo\n// dois-pontos, entao a URL entra sem o \"https://\" e o texto nao usa \":\".\nconst UA_BROWSER = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';\nconst isPortalUrl = /\\/\\/(www\\.)?ooeirense\\.com\\.br(\\/|$|:|\\?)/i.test(articleUrl);\nconst urlCurta = articleUrl.replace(/^https?:\\/\\//i, '');\n\nlet probeStatus = 0;\ntry {\n  const probe = await this.helpers.httpRequest({\n    method: 'GET',\n    url: articleUrl,\n    headers: { 'User-Agent': UA_BROWSER, 'Accept': 'text/html,application/xhtml+xml' },\n    returnFullResponse: true,\n    ignoreHttpStatusErrors: true,\n  });\n  probeStatus = Number(probe.statusCode) || 0;\n} catch (e) {\n  probeStatus = Number(e.statusCode || e.httpCode || (e.response && e.response.status)) || 0;\n  if (!probeStatus) {\n    const mStatus = String(e.message || '').match(/\\b(4\\d\\d|5\\d\\d)\\b/);\n    probeStatus = mStatus ? Number(mStatus[1]) : 0;\n  }\n}\n\n// 404/410 barra em qualquer dominio (pagina nao existe). Os demais 4xx/5xx so\n// barram no proprio portal: em site de terceiro, 403 e bloqueio de bot e o\n// fallback mais abaixo resolve, entao abortar ali seria regressao.\nif (probeStatus === 404 || probeStatus === 410 || (isPortalUrl && probeStatus >= 400)) {\n  throw new Error(\n    'LINK FORA DO AR (HTTP ' + probeStatus + '). Nada foi publicado. '\n    + 'Se o titulo da materia foi editado no painel, o endereco mudou. '\n    + 'Pegue o link atual no portal e mande de novo. Url= ' + urlCurta\n  );\n}\n\nlet title = (txt(meta['og:title']) || txt(jinaData.title) || '').replace(/\\s*[-|].*$/, '').trim();\nlet description = (txt(meta['og:description']) || txt(meta.description) || txt(jinaData.description) || '').trim();\nlet imageUrl = txt(meta['og:image']).trim();\nlet content = txt(jinaData.content);\n\n// Detectar bloqueio do Jina (403 / conteudo vazio / pagina de erro)\nconst jinaBlocked =\n  /403 forbidden|access denied|don't have permission/i.test(title) ||\n  /403 forbidden|don't have permission/i.test(content) ||\n  content.trim().length < 300 ||\n  !imageUrl;\n\nif (jinaBlocked) {\n  try {\n    const html = await this.helpers.httpRequest({\n      method: 'GET',\n      url: articleUrl,\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\n        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n        'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8',\n      },\n      returnFullResponse: false,\n    });\n    const extractMeta = (html, key) => {\n      let rx = new RegExp('<meta[^>]+(?:property|name)\\\\s*=\\\\s*[\"\\'](' + key + ')[\"\\'][^>]*content\\\\s*=\\\\s*[\"\\']([^\"\\']+)[\"\\']', 'i');\n      let m = html.match(rx);\n      if (m) return m[2];\n      rx = new RegExp('<meta[^>]+content\\\\s*=\\\\s*[\"\\']([^\"\\']+)[\"\\'][^>]*(?:property|name)\\\\s*=\\\\s*[\"\\'](' + key + ')[\"\\']', 'i');\n      m = html.match(rx);\n      return m ? m[1] : '';\n    };\n    title = (extractMeta(html, 'og:title') || extractMeta(html, 'twitter:title') || title).replace(/\\s*[-|].*$/, '').trim();\n    description = extractMeta(html, 'og:description') || extractMeta(html, 'twitter:description') || extractMeta(html, 'description') || description;\n    imageUrl = extractMeta(html, 'og:image') || extractMeta(html, 'twitter:image') || imageUrl;\n    const paras = [];\n    const pRx = /<p[^>]*>([\\s\\S]*?)<\\/p>/gi;\n    let mm;\n    while ((mm = pRx.exec(html)) !== null) {\n      const txt = mm[1].replace(/<[^>]+>/g, '').replace(/&nbsp;/g,' ').replace(/&amp;/g,'&').replace(/&quot;/g,'\"').replace(/&#39;/g,\"'\").trim();\n      if (txt.length > 40) paras.push(txt);\n    }\n    content = paras.join('\\n\\n');\n  } catch (e) {\n    // se fallback falhar, deixa campos como estao\n  }\n}\n\n// Decodificar entidades HTML na URL (ex: &amp; -> &)\nimageUrl = imageUrl.replace(/&amp;/g, '&');\n\n// Se a URL for do gerador de OG do portal (/api/og), extrair a imagem real do parâmetro image=\nif (imageUrl && imageUrl.includes('/api/og')) {\n  const imgParam = imageUrl.match(/[?&]image=([^&]+)/);\n  if (imgParam) imageUrl = decodeURIComponent(imgParam[1]);\n}\n\n// CDN Cloudflare\nif (imageUrl.includes('cdn-cgi/image/')) {\n  const originalMatch = imageUrl.match(/\\/https?:\\/\\/.+$/);\n  if (originalMatch) imageUrl = originalMatch[0].substring(1);\n}\n\n// Fallback imagem no markdown\nif (!imageUrl && content) {\n  const skipPatterns = /logo|social-40|icon|favicon|brand|avatar|banner_site/i;\n  const imgRegex = /!\\[.*?\\]\\((https?:\\/\\/[^\\s)]+\\.(jpg|jpeg|png|webp)[^\\s)]*)\\)/gi;\n  let match;\n  while ((match = imgRegex.exec(content)) !== null) {\n    if (!skipPatterns.test(match[1])) { imageUrl = match[1]; break; }\n  }\n}\n\n\n// GUARDA 2 (defesa em profundidade): se o probe acima nao pegou o 404 (rede\n// instavel, CDN servindo cache), a pagina de erro do portal ainda se denuncia\n// pelo titulo e pela capa. Materia de verdade nunca usa og-home.jpg: com capa,\n// o og e o arquivo do Storage; sem capa, e a rota /api/og.\nif (isPortalUrl) {\n  if (/(artigo|p[\\u00e1a]gina)\\s+n[\\u00e3a]o\\s+encontrad[ao]|not found|erro\\s*404/i.test(title)) {\n    throw new Error(\n      'PAGINA DE ERRO DO PORTAL, veio o titulo \"' + title + '\". Nada foi publicado. '\n      + 'O endereco da materia mudou, pegue o link atual no portal. Url= ' + urlCurta\n    );\n  }\n  if (/\\/og-home\\.jpg(\\?|$)/i.test(imageUrl)) {\n    throw new Error(\n      'ESSA URL NAO E DE MATERIA, veio a capa generica do portal (og-home.jpg). '\n      + 'Nada foi publicado. Url= ' + urlCurta\n    );\n  }\n}\n\nconst bodyText = content\n  .replace(/!\\[.*?\\]\\(.*?\\)/g, '')\n  .replace(/\\[([^\\]]+)\\]\\(.*?\\)/g, '$1')\n  .replace(/#{1,6}\\s/g, '')\n  .replace(/[*_]{1,3}/g, '')\n  .replace(/\\n{3,}/g, '\\n\\n')\n  .trim()\n  .substring(0, 3000) || description;\n\nif (!imageUrl || !title || /403 forbidden/i.test(title)) {\n  throw new Error('Falha ao extrair matéria: site bloqueou Jina Reader e fetch direto. Url=' + articleUrl);\n}\n\nreturn [{ json: { url: articleUrl, title, description, imageUrl, bodyText, emailId } }];"
   },
   "id": "aab894d9-b32e-46cf-8a06-a99a01e31f86",
   "name": "Extrair Dados da Matéria",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    1648,
    304
   ],
   "onError": "continueErrorOutput"
  },
  {
   "parameters": {
    "method": "POST",
    "url": "https://api.openai.com/v1/chat/completions",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Authorization",
       "value": "Bearer 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"
      },
      {
       "name": "Content-Type",
       "value": "application/json"
      }
     ]
    },
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ ({\n  \"model\": \"gpt-4o\",\n  \"temperature\": 0.2,\n  \"max_tokens\": 2048,\n  \"response_format\": { \"type\": \"json_object\" },\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"Você é editor do portal O Oeirense (Oeiras, Piauí). Sua função é gerar textos jornalísticos a partir de matérias fornecidas.\\n\\nREGRAS INVIOLÁVEIS:\\n1. NUNCA invente datas, dias da semana, horários, nomes de pessoas, cidades ou números. Copie EXATAMENTE do texto fonte.\\n2. Se uma informação NÃO constar no texto fonte, NÃO a mencione.\\n3. O TÍTULO ORIGINAL e a DESCRIÇÃO são as fontes mais confiáveis. O bodyText pode conter trechos de matérias diferentes — use APENAS o que for claramente relacionado ao título.\\n4. Retorne SEMPRE um JSON válido com os campos: chapeu, titulo, subtitulo, legenda.\\n5. Sem markdown, sem negrito, sem hashtags em nenhum campo.\\n\\nESTRATÉGIA EDITORIAL (engajamento):\\n6. ANCORAGEM LOCAL CONDICIONAL: só destaque Oeiras, uma cidade, pessoa ou lugar quando ele for de fato o ASSUNTO da matéria. Se o fato é estadual, nacional ou de outra cidade e não tem ligação direta com Oeiras ou a região, relate o fato como ele é — não force conexão com Oeiras nem invente relevância local. Nunca afirme que algo impacta Oeiras ou os oeirenses se a fonte não trouxer essa ligação.\\n7. TÍTULOS COM GANCHO: quando a matéria tratar de uma cidade, pessoa ou dado numérico específico, prefira começar o título por esse elemento. Quando o fato for amplo ou genérico, faça um título direto sobre o fato, sem forçar um recorte local. Evite aberturas burocráticas como 'Governo anuncia' ou 'Novo programa'.\\n8. LEGENDA ENVOLVENTE: o primeiro parágrafo deve prender a atenção com o fato mais impactante ou surpreendente. Evite abrir com linguagem burocrática ou institucional.\\n9. PERGUNTA FINAL: a pergunta no último parágrafo deve ser específica ao tema da matéria e provocar opinião ou experiência pessoal do leitor. Nunca use perguntas genéricas como 'O que você achou?'.\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": \"Com base na matéria abaixo, gere:\\n\\n1. CHAPÉU: categoria curta (máximo 2 palavras)\\n2. TÍTULO: máximo 80 caracteres, sem ponto final. REGRA: se a matéria TEM COMO FOCO uma cidade do Piauí, pode começar o título pelo nome dela; se tem como foco uma pessoa conhecida, comece pelo nome dela; se tem um número impactante (R$, quantidade, percentual), pode começar por ele. Mas se o assunto é amplo ou de outra localidade, faça um título direto sobre o fato — não force um nome de cidade que não seja o foco. Exemplos bons: 'Oeiras recebe R$ 2,5 milhões para pavimentação', 'Santa Rosa inaugura nova UBS com atendimento 24h'. Exemplos ruins: 'Governo Federal anuncia investimento para cidade do Piauí', 'Novo equipamento é inaugurado em município'.\\n3. SUBTÍTULO: 2 a 3 frases completas com os detalhes mais importantes. MÍNIMO 160 e MÁXIMO 220 caracteres. O leitor deve entender o essencial. Sem ponto final na última frase\\n4. LEGENDA: texto para Instagram. 4 parágrafos:\\n   - P1 (gancho): fato principal com impacto — abra com o dado mais forte ou surpreendente da própria matéria\\n   - P2 (detalhes): contexto e informações complementares\\n   - P3 (significado): por que o fato importa. Se ele afeta Oeiras ou a região, explique esse impacto local; se for de âmbito mais amplo, explique a relevância real do fato sem inventar um efeito local\\n   - P4 (conversa): observação final + pergunta ESPECÍFICA que provoque opinião ou experiência pessoal\\n   Cada parágrafo com 2-3 frases. Sem links, sem CTA genérico, sem hashtags.\\n\\nLEMBRETE: NÃO invente nenhum dado. Use APENAS o que está no texto abaixo.\\n\\nTítulo original: \" + $json.title + \"\\nDescrição: \" + $json.description + \"\\nTexto: \" + ($json.bodyText || '').substring(0, 3000)\n    }\n  ]\n}) }}",
    "options": {}
   },
   "id": "57d5fda0-77f8-4a5f-9c75-9775825fae8c",
   "name": "OpenAI - Gerar Textos",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    1888,
    304
   ]
  },
  {
   "parameters": {
    "jsCode": "const response = $input.first().json;\nconst content = response.choices[0].message.content;\n\nconst jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\nif (!jsonMatch) throw new Error('JSON não encontrado na resposta da OpenAI: ' + content);\n\nconst parsed = JSON.parse(jsonMatch[0]);\n\nreturn [{\n  json: {\n    chapeu:    parsed.chapeu    || '',\n    titulo:    parsed.titulo    || '',\n    subtitulo: parsed.subtitulo || '',\n    legenda:   parsed.legenda   || '',\n    imageUrl:  $('Extrair Dados da Matéria').first().json.imageUrl,\n    url:       $('Extrair Dados da Matéria').first().json.url,\n    emailId:   $('Extrair Dados da Matéria').first().json.emailId\n  }\n}];"
   },
   "id": "78477a9f-5b51-4582-8f8d-17ca8c893219",
   "name": "Parsear Resposta OpenAI",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    2128,
    304
   ]
  },
  {
   "parameters": {
    "url": "={{ $json.imageUrl }}",
    "options": {
     "response": {
      "response": {
       "responseFormat": "file"
      }
     }
    }
   },
   "id": "bb000001-0001-0001-0001-000000000001",
   "name": "Baixar Imagem",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    2368,
    304
   ]
  },
  {
   "parameters": {
    "jsCode": "const item = $input.first();\nconst binaryKey = Object.keys(item.binary)[0];\nconst binaryMeta = item.binary[binaryKey];\nconst mimeType = binaryMeta.mimeType || 'image/jpeg';\n\n// Debug: mostrar o que binaryMeta contém\nconst metaKeys = Object.keys(binaryMeta);\nconst dataLen = binaryMeta.data ? binaryMeta.data.length : 0;\nconst dataPreview = binaryMeta.data ? binaryMeta.data.substring(0, 120) : 'VAZIO';\n\n// Se data existe e é grande (>500 chars), é base64 real\n// Se é curto, é um ID de filesystem — precisamos do helpers\nlet base64;\n\nif (dataLen > 500) {\n  base64 = binaryMeta.data;\n} else {\n  // Tentar helpers\n  try {\n    const buffer = await this.helpers.getBinaryDataBuffer(0, binaryKey);\n    base64 = buffer.toString('base64');\n  } catch(e) {\n    throw new Error(\n      'Não conseguiu ler binário! ' +\n      'data.length=' + dataLen +\n      ' | data.preview=' + dataPreview +\n      ' | keys=' + metaKeys.join(',') +\n      ' | helpers=' + (typeof this.helpers) +\n      ' | error=' + e.message\n    );\n  }\n}\n\nconst prev = $('Parsear Resposta OpenAI').first().json;\nreturn [{ json: { ...prev, imageBase64: 'data:' + mimeType + ';base64,' + base64 } }];"
   },
   "id": "bb000001-0001-0001-0001-000000000002",
   "name": "Converter Base64",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    2608,
    304
   ]
  },
  {
   "parameters": {
    "jsCode": "const prev = $('Parsear Resposta OpenAI').first().json;\nconst { chapeu, titulo, subtitulo, legenda, imageUrl, url, emailId } = prev;\nconst imageBase64 = $json.imageBase64;\n\nconst tLen = (titulo || '').length;\nconst tFS = tLen > 95 ? 46 : tLen > 80 ? 50 : tLen > 60 ? 56 : 62;\nconst sLen = (subtitulo || '').length;\nconst sFS = sLen > 220 ? 28 : sLen > 170 ? 30 : 32;\n\n\nconst html = `<!DOCTYPE html>\n<html lang=\"pt-BR\"><head>\n<meta charset=\"UTF-8\">\n<link href=\"https://fonts.googleapis.com/css2?family=Raleway:wght@400;500;600;700;800;900&display=swap\" rel=\"stylesheet\">\n<style>\n  *{margin:0;padding:0;box-sizing:border-box}\n  html,body{width:1080px;height:1440px;overflow:hidden;font-family:'Raleway',sans-serif;background:#0d1f2d}\n  .post{position:relative;width:1080px;height:1440px;background:#0d1f2d;display:flex;flex-direction:column;overflow:hidden}\n\n  .img-wrap{position:relative;width:100%;height:820px;overflow:hidden;background:#0a0f14;flex-shrink:0}\n  .img-wrap img{width:100%;height:100%;object-fit:cover;object-position:center top;display:block}\n  .img-wrap::after{content:'';position:absolute;left:0;right:0;bottom:0;height:140px;background:linear-gradient(to top,rgba(13,31,45,.35),transparent);pointer-events:none}\n\n  .accent-line{height:4px;width:100%;background:linear-gradient(to right,#127b9d 0%,#3ea081 50%,#62c069 100%);flex-shrink:0}\n\n  .chapeu{position:absolute;left:68px;top:796px;z-index:5;display:inline-flex;align-items:center;gap:14px;padding:14px 28px;background-image:linear-gradient(to right,#127b9d 0%,#3ea081 50%,#62c069 100%);background-size:1080px 100%;background-position:-68px 0;background-repeat:no-repeat;color:#fff;font-size:21px;font-weight:800;letter-spacing:5.2px;text-transform:uppercase;border-radius:4px;box-shadow:0 8px 32px rgba(0,0,0,.35)}\n  .chapeu::before{content:'';width:10px;height:10px;background:#FFDE00;border-radius:50%;flex-shrink:0}\n\n  .content{flex:1;padding:52px 68px 0 68px;display:flex;flex-direction:column;justify-content:flex-start;position:relative;background-color:#0d1f2d;background-image:radial-gradient(circle at 18% 0%,rgba(255,255,255,.035) 0%,transparent 55%)}\n\n  .titulo{font-size:${tFS}px;line-height:1.13;font-weight:900;color:#fff;letter-spacing:-0.6px;margin-bottom:28px;max-width:100%}\n  .subtitulo{font-size:${sFS}px;line-height:1.42;font-weight:400;color:rgba(255,255,255,.72);max-width:100%}\n\n  .footer{position:absolute;bottom:0;left:0;right:0;height:110px;padding:0 68px;display:flex;align-items:center;justify-content:space-between;background:linear-gradient(to right,#127b9d 0%,#3ea081 50%,#62c069 100%)}\n  .footer-url,.footer-handle{font-size:18px;font-weight:700;color:#fff;letter-spacing:0.5px}\n  </style>\n</head>\n<body>\n  <div class=\"post\">\n    <div class=\"img-wrap\"><img src=\"${imageBase64}\"></div>\n    <div class=\"accent-line\"></div>\n    <div class=\"chapeu\">${chapeu}</div>\n    <div class=\"content\">\n      <div>\n        <h1 class=\"titulo\">${titulo}</h1>\n        <p class=\"subtitulo\">${subtitulo}</p>\n      </div>\n      <div class=\"footer\">\n        <span class=\"footer-url\">ooeirense.com.br</span>\n        <span class=\"footer-handle\">@ooeirense</span>\n      </div>\n    </div>\n  </div>\n</body></html>`;\n\nreturn [{ json: { html, chapeu, titulo, subtitulo, legenda, imageUrl, url, emailId } }];"
   },
   "id": "7793f67c-b306-4b6d-9c16-3060d84e1512",
   "name": "Montar HTML",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    2848,
    304
   ]
  },
  {
   "parameters": {
    "jsCode": "\nconst html = $json.html;\n\nconst resp = await this.helpers.httpRequest({\n  method: 'POST',\n  url: 'http://127.0.0.1:3099/screenshot',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ html, width: 1080, height: 1440, filename: `instagram/post_${Date.now()}.png` })\n});\n\nif (!resp.url) throw new Error('Screenshot server erro: ' + JSON.stringify(resp));\nreturn [{ json: { url: resp.url } }];\n"
   },
   "id": "f1ac146d-ee8c-4cff-8fdf-2e7d943b43a5",
   "name": "HCTI - HTML para PNG",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    3088,
    304
   ],
   "onError": "continueErrorOutput"
  },
  {
   "parameters": {
    "jsCode": "// Guarda a URL da imagem gerada e passa os dados anteriores\nconst imagemGerada = $input.first().json.url;\nif (!imagemGerada) throw new Error('HCTI não retornou URL da imagem: ' + JSON.stringify($input.first().json));\n\nreturn [{\n  json: {\n    imagemGerada,\n    legenda:  $('Montar HTML').first().json.legenda,\n    titulo:   $('Montar HTML').first().json.titulo,\n    emailId:  $('Montar HTML').first().json.emailId\n  }\n}];"
   },
   "id": "1372e78b-6c7f-42b0-a91f-ad66b241ed49",
   "name": "Capturar URL da Imagem",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    3328,
    304
   ]
  },
  {
   "parameters": {
    "method": "POST",
    "url": "https://graph.facebook.com/v25.0/17841477402712235/media",
    "sendBody": true,
    "contentType": "form-urlencoded",
    "bodyParameters": {
     "parameters": [
      {
       "name": "image_url",
       "value": "={{ $json.imagemGerada }}"
      },
      {
       "name": "caption",
       "value": "={{ $json.legenda }}"
      },
      {
       "name": "access_token",
       "value": "EAAVw81fCrxUBRLOnGwCYAexfTBk6BLWezW627TNjp4bEQMGBcaCmhWV0TTuNzLl0N9TqDSXniD3ZBSOiwevn9YTXKE7bGBDZBwqAGzRRpeOTmSTEjYvqDZBqV3nv8tLMEKlyCo6Xl23AkX7ZBoJ8t3hjUmh0HIDnIicoFEoEFbOeddWkpxQHHRUnMG0PgQZDZD"
      }
     ]
    },
    "options": {}
   },
   "id": "f9194a57-93f8-49e3-ac8a-f1609934aa23",
   "name": "Instagram - Criar Container",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    3568,
    304
   ],
   "onError": "continueErrorOutput"
  },
  {
   "parameters": {
    "amount": 30
   },
   "id": "cb0968d3-379b-47a3-b7b4-6d117dda59f7",
   "name": "Aguardar Processamento",
   "type": "n8n-nodes-base.wait",
   "typeVersion": 1.1,
   "position": [
    3808,
    304
   ],
   "webhookId": "ooeirense-email-aguardar"
  },
  {
   "parameters": {
    "method": "POST",
    "url": "https://graph.facebook.com/v25.0/17841477402712235/media_publish",
    "sendBody": true,
    "contentType": "form-urlencoded",
    "bodyParameters": {
     "parameters": [
      {
       "name": "creation_id",
       "value": "={{ $json.id }}"
      },
      {
       "name": "access_token",
       "value": "EAAVw81fCrxUBRLOnGwCYAexfTBk6BLWezW627TNjp4bEQMGBcaCmhWV0TTuNzLl0N9TqDSXniD3ZBSOiwevn9YTXKE7bGBDZBwqAGzRRpeOTmSTEjYvqDZBqV3nv8tLMEKlyCo6Xl23AkX7ZBoJ8t3hjUmh0HIDnIicoFEoEFbOeddWkpxQHHRUnMG0PgQZDZD"
      }
     ]
    },
    "options": {}
   },
   "id": "6ecec3a3-18a2-4479-ad3f-22e0bfd882d4",
   "name": "Instagram - Publicar",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    4048,
    304
   ],
   "onError": "continueErrorOutput"
  },
  {
   "parameters": {
    "jsCode": "const prev = $('Parsear Resposta OpenAI').first().json;\nconst { chapeu, titulo, subtitulo, legenda, imageUrl, url, emailId } = prev;\nconst imageBase64 = $('Converter Base64').first().json.imageBase64;\n\nconst tLen = (titulo || '').length;\nconst tFS = tLen > 95 ? 50 : tLen > 80 ? 56 : tLen > 60 ? 62 : 68;\nconst sLen = (subtitulo || '').length;\nconst sFS = sLen > 220 ? 28 : sLen > 170 ? 30 : 32;\n\n\nconst html = `<!DOCTYPE html>\n<html lang=\"pt-BR\"><head>\n<meta charset=\"UTF-8\">\n<link href=\"https://fonts.googleapis.com/css2?family=Raleway:wght@400;500;600;700;800;900&display=swap\" rel=\"stylesheet\">\n<style>\n  *{margin:0;padding:0;box-sizing:border-box}\n  html,body{width:1080px;height:1920px;overflow:hidden;font-family:'Raleway',sans-serif;background:#0d1f2d}\n  .post{position:relative;width:1080px;height:1920px;background:#0d1f2d;display:flex;flex-direction:column;overflow:hidden}\n\n  .img-wrap{position:relative;width:100%;height:1100px;overflow:hidden;background:#0a0f14;flex-shrink:0}\n  .img-wrap img{width:100%;height:100%;object-fit:cover;object-position:center top;display:block}\n  .img-wrap::after{content:'';position:absolute;left:0;right:0;bottom:0;height:170px;background:linear-gradient(to top,rgba(13,31,45,.35),transparent);pointer-events:none}\n\n  .accent-line{height:4px;width:100%;background:linear-gradient(to right,#127b9d 0%,#3ea081 50%,#62c069 100%);flex-shrink:0}\n\n  .chapeu{position:absolute;left:68px;top:1076px;z-index:5;display:inline-flex;align-items:center;gap:14px;padding:14px 28px;background-image:linear-gradient(to right,#127b9d 0%,#3ea081 50%,#62c069 100%);background-size:1080px 100%;background-position:-68px 0;background-repeat:no-repeat;color:#fff;font-size:21px;font-weight:800;letter-spacing:5.2px;text-transform:uppercase;border-radius:4px;box-shadow:0 8px 32px rgba(0,0,0,.35)}\n  .chapeu::before{content:'';width:10px;height:10px;background:#FFDE00;border-radius:50%;flex-shrink:0}\n\n  .content{flex:1;padding:52px 68px 32px 68px;display:flex;flex-direction:column;position:relative;background-color:#0d1f2d;background-image:radial-gradient(circle at 18% 0%,rgba(255,255,255,.035) 0%,transparent 55%)}\n\n  .titulo{font-size:${tFS}px;line-height:1.13;font-weight:900;color:#fff;letter-spacing:-0.6px;margin-bottom:30px;max-width:100%}\n  .subtitulo{font-size:${sFS}px;line-height:1.45;font-weight:400;color:rgba(255,255,255,.72);max-width:100%}\n  .footer{flex-shrink:0;height:110px;padding:0 68px;display:flex;align-items:center;justify-content:space-between;background:linear-gradient(to right,#127b9d 0%,#3ea081 50%,#62c069 100%)}\n  .footer-url,.footer-handle{font-size:18px;font-weight:700;color:#fff;letter-spacing:0.5px}\n  </style>\n</head>\n<body>\n  <div class=\"post\">\n    <div class=\"img-wrap\"><img src=\"${imageBase64}\"></div>\n    <div class=\"accent-line\"></div>\n    <div class=\"chapeu\">${chapeu}</div>\n    <div class=\"content\">\n      <h1 class=\"titulo\">${titulo}</h1>\n      <p class=\"subtitulo\">${subtitulo}</p>\n    </div>\n    <div class=\"footer\">\n        <span class=\"footer-url\">ooeirense.com.br</span>\n      <span class=\"footer-handle\">@ooeirense</span>\n    </div>\n  </div>\n</body></html>`;\n\nreturn [{ json: { htmlStory: html, chapeu, titulo, subtitulo, legenda, imageUrl, url, emailId } }];"
   },
   "id": "eb78bbc3-6f92-491f-a02e-d794a324cd24",
   "name": "Montar HTML Story",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    4288,
    304
   ]
  },
  {
   "parameters": {
    "jsCode": "\nconst html = $json.htmlStory;\n\nconst resp = await this.helpers.httpRequest({\n  method: 'POST',\n  url: 'http://127.0.0.1:3099/screenshot',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ html, width: 1080, height: 1920, filename: `instagram/story_${Date.now()}.png` })\n});\n\nif (!resp.url) throw new Error('Screenshot server erro: ' + JSON.stringify(resp));\nreturn [{ json: { url: resp.url } }];\n"
   },
   "id": "f318a2bf-aa02-4490-ad14-63a79b23bb14",
   "name": "HCTI - Story PNG",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    4528,
    304
   ],
   "onError": "continueErrorOutput"
  },
  {
   "parameters": {
    "jsCode": "const urlStory = $input.first().json.url;\nif (!urlStory) throw new Error('HCTI não retornou URL do story: ' + JSON.stringify($input.first().json));\nreturn [{ json: { urlStory, emailId: $('Montar HTML Story').first().json.emailId } }];"
   },
   "id": "b39c0fe9-31c4-4b1a-9c70-e84be83563bb",
   "name": "Capturar URL Story",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    4768,
    304
   ]
  },
  {
   "parameters": {
    "url": "=https://api.telegram.org/bot8684225036:AAHPUgR6b1y7oN7y5q1FMQ7vcoi3Io9nysE/sendMessage",
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify({ chat_id: '7769073261', text: '✅ Publicado no @ooeirense!\\n\\n📰 ' + $('Montar HTML').first().json.titulo + '\\n\\nPost + Story publicados com sucesso.', parse_mode: 'HTML' }) }}",
    "options": {}
   },
   "id": "tg-confirm",
   "name": "Telegram - Confirmar Publicação",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    5728,
    304
   ]
  },
  {
   "parameters": {
    "jsCode": "const prev = $('Extrair URL do Telegram').first().json;\nif (prev.duplicate) { return []; }\nreturn [{ json: { url: prev.url, messageId: prev.messageId } }];"
   },
   "id": "pass-url",
   "name": "Repassar URL",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    1008,
    304
   ]
  },
  {
   "parameters": {
    "method": "POST",
    "url": "https://api.telegram.org/bot8684225036:AAHPUgR6b1y7oN7y5q1FMQ7vcoi3Io9nysE/sendMessage",
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify({ chat_id: '7769073261', text: '❌ <b>Erro ao publicar no @ooeirense</b>\\n\\n' + (typeof $json.error === 'string' ? $json.error : ($json.error?.message || $json.message || JSON.stringify($json))).substring(0, 350), parse_mode: 'HTML' }) }}",
    "options": {}
   },
   "id": "tg-erro-001",
   "name": "Telegram - Erro",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    2896,
    1296
   ]
  },
  {
   "parameters": {
    "path": "dispatch-instagram",
    "httpMethod": "POST",
    "responseMode": "onReceived",
    "options": {}
   },
   "id": "wh-dispatch-instagram",
   "name": "Webhook Dispatcher",
   "type": "n8n-nodes-base.webhook",
   "typeVersion": 2,
   "position": [
    208,
    304
   ],
   "webhookId": "wh-dispatch-instagram"
  },
  {
   "parameters": {
    "method": "POST",
    "url": "https://graph.facebook.com/v25.0/17841477402712235/media",
    "sendBody": true,
    "contentType": "form-urlencoded",
    "bodyParameters": {
     "parameters": [
      {
       "name": "media_type",
       "value": "STORIES"
      },
      {
       "name": "image_url",
       "value": "={{ $json.urlStory }}"
      },
      {
       "name": "access_token",
       "value": "EAAVw81fCrxUBRLOnGwCYAexfTBk6BLWezW627TNjp4bEQMGBcaCmhWV0TTuNzLl0N9TqDSXniD3ZBSOiwevn9YTXKE7bGBDZBwqAGzRRpeOTmSTEjYvqDZBqV3nv8tLMEKlyCo6Xl23AkX7ZBoJ8t3hjUmh0HIDnIicoFEoEFbOeddWkpxQHHRUnMG0PgQZDZD"
      }
     ]
    },
    "options": {}
   },
   "id": "story-criar-container",
   "name": "Story - Criar Container",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    5248,
    304
   ],
   "onError": "continueErrorOutput"
  },
  {
   "parameters": {
    "amount": 20
   },
   "id": "story-aguardar",
   "name": "Story - Aguardar",
   "type": "n8n-nodes-base.wait",
   "typeVersion": 1.1,
   "position": [
    5488,
    304
   ],
   "webhookId": "ooeirense-story-aguardar"
  },
  {
   "parameters": {
    "method": "POST",
    "url": "https://graph.facebook.com/v25.0/17841477402712235/media_publish",
    "sendBody": true,
    "contentType": "form-urlencoded",
    "bodyParameters": {
     "parameters": [
      {
       "name": "creation_id",
       "value": "={{ $json.id }}"
      },
      {
       "name": "access_token",
       "value": "EAAVw81fCrxUBRLOnGwCYAexfTBk6BLWezW627TNjp4bEQMGBcaCmhWV0TTuNzLl0N9TqDSXniD3ZBSOiwevn9YTXKE7bGBDZBwqAGzRRpeOTmSTEjYvqDZBqV3nv8tLMEKlyCo6Xl23AkX7ZBoJ8t3hjUmh0HIDnIicoFEoEFbOeddWkpxQHHRUnMG0PgQZDZD"
      }
     ]
    },
    "options": {}
   },
   "id": "story-publicar",
   "name": "Story - Publicar",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    5728,
    304
   ],
   "onError": "continueErrorOutput"
  }
 ],
 "connections": {
  "Extrair URL do Telegram": {
   "main": [
    [
     {
      "node": "Repassar URL",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Buscar Matéria": {
   "main": [
    [
     {
      "node": "Extrair Dados da Matéria",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Extrair Dados da Matéria": {
   "main": [
    [
     {
      "node": "OpenAI - Gerar Textos",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Telegram - Erro",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "OpenAI - Gerar Textos": {
   "main": [
    [
     {
      "node": "Parsear Resposta OpenAI",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Parsear Resposta OpenAI": {
   "main": [
    [
     {
      "node": "Baixar Imagem",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Baixar Imagem": {
   "main": [
    [
     {
      "node": "Converter Base64",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Converter Base64": {
   "main": [
    [
     {
      "node": "Montar HTML",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Montar HTML": {
   "main": [
    [
     {
      "node": "HCTI - HTML para PNG",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "HCTI - HTML para PNG": {
   "main": [
    [
     {
      "node": "Capturar URL da Imagem",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Telegram - Erro",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Capturar URL da Imagem": {
   "main": [
    [
     {
      "node": "Instagram - Criar Container",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Instagram - Criar Container": {
   "main": [
    [
     {
      "node": "Aguardar Processamento",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Telegram - Erro",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Aguardar Processamento": {
   "main": [
    [
     {
      "node": "Instagram - Publicar",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Instagram - Publicar": {
   "main": [
    [
     {
      "node": "Montar HTML Story",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Telegram - Erro",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Repassar URL": {
   "main": [
    [
     {
      "node": "Buscar Matéria",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Webhook Dispatcher": {
   "main": [
    [
     {
      "node": "Extrair URL do Telegram",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Montar HTML Story": {
   "main": [
    [
     {
      "node": "HCTI - Story PNG",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "HCTI - Story PNG": {
   "main": [
    [
     {
      "node": "Capturar URL Story",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Telegram - Erro",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Capturar URL Story": {
   "main": [
    [
     {
      "node": "Story - Criar Container",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Story - Criar Container": {
   "main": [
    [
     {
      "node": "Story - Aguardar",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Telegram - Erro",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Story - Aguardar": {
   "main": [
    [
     {
      "node": "Story - Publicar",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Story - Publicar": {
   "main": [
    [
     {
      "node": "Telegram - Confirmar Publicação",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Telegram - Erro",
      "type": "main",
      "index": 0
     }
    ]
   ]
  }
 },
 "settings": {
  "executionOrder": "v1",
  "callerPolicy": "workflowsFromSameOwner",
  "availableInMCP": false,
  "binaryMode": "separate"
 },
 "staticData": {
  "global": {
   "lastUpdateId": 900183722628,
   "seenUrls": {
    "https://ooeirense.com.br/cidades/a-noticia-mudou-de-endereco-54-ja-se-informam-pelo-meio-digital-37-pela-tv-e-radio-registra-4-da-audiencia-geral": 1780843873929
   }
  },
  "node:Poll Telegram": {
   "recurrenceRules": []
  }
 },
 "pinData": {},
 "versionId": "ecc05eb4-81a8-4621-9deb-d768c3001d31",
 "triggerCount": 1,
 "meta": null,
 "parentFolderId": null,
 "createdAt": "2026-04-12 23:18:47.196",
 "updatedAt": "2026-08-25 15:19:21.135",
 "isArchived": 0,
 "versionCounter": 1228,
 "description": null,
 "activeVersionId": "ecc05eb4-81a8-4621-9deb-d768c3001d31"
}