{
  "name": "Postagem Redes \u2014 Portal: Arquivos",
  "description": null,
  "isArchived": false,
  "nodes": [
    {
      "parameters": {
        "content": "## Arquivos protegidos\n\nEntrega somente m\u00eddia pertencente a um conte\u00fado conhecido.\n\nQuando a URL p\u00fablica estiver habilitada, valida expira\u00e7\u00e3o e assinatura HMAC antes de ler o arquivo.",
        "width": 520,
        "height": 175,
        "color": 5
      },
      "name": "Resumo operacional",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -360,
        -290
      ]
    },
    {
      "parameters": {
        "httpMethod": "GET",
        "path": "postagem-redes-arquivo",
        "responseMode": "responseNode",
        "options": {}
      },
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -280,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction scan(){\n  fs.mkdirSync(INPUT,{recursive:true});\n  const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n  const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n  const items=[];\n  for(const folder of folders){\n    const dir=path.join(INPUT,folder);\n    const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n    const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n    const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n    const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n    const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n    const id=slug(folder); const old=state.records[id]||{};\n    const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n    items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n  }\n  return {state,items};\n}\n\nconst query=$input.first().json.query||{}; const id=safeText(query.id,130); const file=safeText(query.file,200); if(!id||!file||path.basename(file)!==file||!/^.+\\.(png|jpe?g|webp)$/i.test(file)) throw new Error('Arquivo inv\u00e1lido.'); const requireSignature=String($env.SOCIAL_MEDIA_REQUIRE_SIGNED_URLS||'').toLowerCase()==='true'; if(requireSignature){const exp=Number(query.exp); const supplied=String(query.sig||''); const secret=String($env.SOCIAL_MEDIA_SIGNING_SECRET||''); if(!secret||!Number.isFinite(exp)||exp<Math.floor(Date.now()/1000)||exp>Math.floor(Date.now()/1000)+21600||!supplied) throw new Error('Link de m\u00eddia ausente, expirado ou inv\u00e1lido.'); const expected=crypto.createHmac('sha256',secret).update(id+':'+file+':'+exp).digest('base64url'); const valid=supplied.length===expected.length&&crypto.timingSafeEqual(Buffer.from(supplied),Buffer.from(expected)); if(!valid) throw new Error('Assinatura de m\u00eddia inv\u00e1lida.')} const {items}=scan(); const content=items.find(x=>x.id===id); if(!content||!content.slides.includes(file)) throw new Error('Imagem n\u00e3o encontrada.'); const full=path.join(INPUT,content.folder,file); const data=fs.readFileSync(full); const mime=/\\.png$/i.test(file)?'image/png':/\\.webp$/i.test(file)?'image/webp':'image/jpeg'; return [{json:{},binary:{data:{data:data.toString('base64'),mimeType:mime,fileName:file}}}];"
      },
      "name": "Processar",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "respondWith": "binary",
        "responseBinaryPropertyName": "data",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Cache-Control",
                "value": "private, max-age=604800, immutable"
              }
            ]
          }
        }
      },
      "name": "Responder",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        280,
        0
      ]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Processar",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Processar": {
      "main": [
        [
          {
            "node": "Responder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "availableInMCP": false
  },
  "nodeGroups": [],
  "activeVersionId": "a6214fa8-c892-4a94-a29c-7f020df86b3c",
  "versionCounter": 6,
  "triggerCount": 1,
  "sourceWorkflowId": null,
  "versionMetadata": {
    "name": "Postagem Redes \u2014 Portal: Arquivos",
    "description": null
  },
  "active": false
}