{
  "name": "MADIAD Hub - Đăng theo lịch từ Google Sheets",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 15
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -160,
        0
      ],
      "id": "Chạy mỗi 15 phút",
      "name": "Chạy mỗi 15 phút"
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "[MADIAD Template] Lịch Đăng Hub"
        },
        "sheetName": {
          "__rl": true,
          "value": "Cấu Hình",
          "mode": "name"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "executeOnce": true,
      "position": [
        60,
        0
      ],
      "id": "Đọc Cấu Hình",
      "name": "Đọc Cấu Hình"
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "[MADIAD Template] Lịch Đăng Hub"
        },
        "sheetName": {
          "__rl": true,
          "value": "Lịch Đăng",
          "mode": "name"
        },
        "options": {
          "dataLocationOnSheet": {
            "values": {
              "rangeDefinition": "specifyRange",
              "headerRow": 1,
              "firstDataRow": 3
            }
          }
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "executeOnce": true,
      "position": [
        280,
        0
      ],
      "id": "Đọc Lịch Đăng",
      "name": "Đọc Lịch Đăng"
    },
    {
      "parameters": {
        "jsCode": "\nconst cfg = {};\nfor (const it of $('Đọc Cấu Hình').all()) {\n  const k = String(it.json['Khoá'] ?? '').trim();\n  if (k) cfg[k] = String(it.json['Giá Trị'] ?? '').trim();\n}\nif (!cfg.profile_id) throw new Error('Chưa điền profile_id ở tab Cấu Hình. Lấy trong Dashboard MADIAD Hub.');\nconst KENH = { 'Facebook': 'facebook', 'Instagram': 'instagram', 'TikTok': 'tiktok', 'YouTube': 'youtube', 'LinkedIn': 'linkedin', 'X': 'x', 'Threads': 'threads', 'Pinterest': 'pinterest', 'Reddit': 'reddit', 'Bluesky': 'bluesky', 'Google Business': 'google_business', 'Telegram': 'telegram', 'Discord': 'discord', 'Slack': 'slack', 'Mastodon': 'mastodon', 'Nostr': 'nostr', 'Lemmy': 'lemmy', 'dev.to': 'devto', 'Hashnode': 'hashnode', 'WordPress': 'wordpress', 'Whop': 'whop', 'listmonk': 'listmonk' };\nconst IMG = ['.jpg', '.jpeg', '.png', '.webp', '.gif'];\nconst VID = ['.mp4', '.mov', '.m4v', '.webm'];\nconst NEEDS_TITLE = ['youtube', 'reddit', 'pinterest'];\nconst OPEN = ['', 'Nháp', 'Chờ Đăng'];\n\nconst SYS_COLS_ = ['Mã Bài', 'Trạng Thái', 'Mã Yêu Cầu', 'Link Bài Đăng',\n  'Mã Bài Đăng', 'Đăng Lúc', 'Lỗi'];\nfunction hash32_(s, h) {\n  for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619) >>> 0; }\n  return h >>> 0;\n}\n// Namespace = which sheet + which profile. The gateway scopes keys per customer, not per sheet: two\n// copies of the template under one API key with the same content at the same row on the same day\n// would otherwise share a key (review 22/09). Both wf-01 and wf-05 read the same sheet, so both see\n// the same documentId. Reading it can fail in an old n8n — then the profile alone still separates.\nfunction sheetNs_(cfg) {\n  let doc = '';\n  try { const d = $('Đọc Lịch Đăng').params.documentId; doc = String((d && (d.value ?? d)) || ''); } catch (e) { doc = ''; }\n  return String(cfg.profile_id || '') + '|' + doc;\n}\n// Known limit, accepted: two runs that straddle midnight in the configured timezone (seconds apart)\n// get different day parts and two keys. Dropping the day is worse — a row cleared and re-pasted with\n// the same content on a later day would replay the old result and publish NOTHING, silently.\nfunction rowKey(r, tz, nowMs, ns) {\n  const skip = new Set(SYS_COLS_.map(x => String(x).trim().toLowerCase()));\n  const s = String(ns ?? '') + '\u0003' + Object.keys(r).filter(k => k !== 'row_number' && !skip.has(String(k).trim().toLowerCase()))\n    .sort().map(k => k + '\\u0001' + String(r[k] ?? '').trim()).join('\\u0002');\n  const h = hash32_(s, 2166136261).toString(16).padStart(8, '0') + hash32_(s, 3735928559).toString(16).padStart(8, '0');\n  let day;\n  try { day = (nowMs == null ? DateTime.now() : DateTime.fromMillis(nowMs)).setZone(tz).toFormat('yyyyMMdd'); }\n  catch (e) { day = new Date(nowMs ?? Date.now()).toISOString().slice(0, 10).replace(/-/g, ''); }\n  if (!/^\\d{8}$/.test(day)) day = new Date(nowMs ?? Date.now()).toISOString().slice(0, 10).replace(/-/g, '');\n  return 'P-' + r.row_number + '-' + day + '-' + h;\n}\n\nfunction buildPost(r, cfg, KENH, IMG, VID, NEEDS_TITLE) {\n  const caption = String(r['Nội Dung'] ?? '').trim();\n  const raw = String(r['Kênh Đăng'] ?? '').trim();\n  // Dòng chưa soạn xong thì bỏ qua im lặng, không coi là lỗi.\n  if (!caption || !raw) return null;\n\n  const platforms = raw.split(',').map(s => KENH[s.trim()]).filter(Boolean);\n  if (!platforms.length) return { _row: r.row_number, _err: 'Cột Kênh Đăng không khớp kênh nào. Chọn lại từ dropdown.' };\n\n  const links = String(r['Media'] ?? '').split(/[\\n,]/).map(s => s.trim()).filter(Boolean);\n  const bad = links.filter(l => !l.startsWith('https://'));\n  if (bad.length) return { _row: r.row_number, _err: 'Link media phải bắt đầu bằng https:// , đang là: ' + bad[0] };\n\n  const low = links.map(l => l.toLowerCase());\n  let endpoint = 'text';\n  if (links.length) endpoint = low.some(l => VID.some(e => l.includes(e))) ? 'video' : 'photos';\n\n  const body = { profile_id: cfg.profile_id, platforms, caption };\n  const title = String(r['Tiêu Đề'] ?? '').trim();\n  if (title) for (const p of platforms) if (NEEDS_TITLE.includes(p)) body[p + '_title'] = title;\n  const fc = String(r['Bình Luận Đầu'] ?? '').trim();\n  if (fc) body.first_comment = fc;\n  if (platforms.includes('pinterest')) {\n    if (!cfg.pinterest_board_id) return { _row: r.row_number, _err: 'Đăng Pinterest cần pinterest_board_id ở tab Cấu Hình.' };\n    body.pinterest_board_id = cfg.pinterest_board_id;\n  }\n  if (endpoint === 'video') body.video_url = links[0];\n  if (endpoint === 'photos') body.photo_urls = links;\n\n  // Mã Bài là chìa khoá chống đăng trùng. Sinh từ chính DÒNG (số dòng + nội dung bạn gõ + ngày), TRƯỚC khi gọi API, rồi ghi ngay xuống sheet — nên hai lượt chạy cùng đọc một dòng (webhook và lượt quét) ra cùng một chìa, và Hub chỉ đăng một lần.\n  const id = String(r['Mã Bài'] ?? '').trim() || rowKey(r, cfg.timezone || 'Asia/Ho_Chi_Minh', null, sheetNs_(cfg));\n  return { _row: r.row_number, _id: id, _endpoint: endpoint, body };\n}\n\nconst perRun = parseInt(cfg.rows_per_run || '10', 10) || 10;\nconst out = [];\nfor (const it of $input.all()) {\n  if (out.length >= perRun) break;\n  const r = it.json;\n  if (String(r['Hành Động'] ?? '').trim() !== 'Đăng ngay') continue;\n  if (!OPEN.includes(String(r['Trạng Thái'] ?? '').trim())) continue;\n  const b = buildPost(r, cfg, KENH, IMG, VID, NEEDS_TITLE);\n  if (b) out.push({ json: b });\n}\nreturn out;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        500,
        0
      ],
      "id": "Chuẩn bị bài đăng",
      "name": "Chuẩn bị bài đăng"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "version": 2
          },
          "conditions": [
            {
              "id": "c",
              "operator": {
                "type": "string",
                "operation": "notExists"
              },
              "leftValue": "={{ $json._err }}",
              "rightValue": ""
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        720,
        0
      ],
      "id": "Dòng hợp lệ?",
      "name": "Dòng hợp lệ?"
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "[MADIAD Template] Lịch Đăng Hub"
        },
        "sheetName": {
          "__rl": true,
          "value": "Lịch Đăng",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "row_number": "={{ $json._row }}",
            "Mã Bài": "={{ $json._id }}",
            "Trạng Thái": "Đang Đăng",
            "Lỗi": ""
          },
          "matchingColumns": [
            "row_number"
          ],
          "schema": []
        },
        "options": {
          "dataLocationOnSheet": {
            "values": {
              "rangeDefinition": "specifyRange",
              "headerRow": 1,
              "firstDataRow": 3
            }
          }
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        940,
        -120
      ],
      "id": "Đánh dấu Đang Đăng",
      "name": "Đánh dấu Đang Đăng",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.madiad.com/v1/posts/{{ $('Chuẩn bị bài đăng').item.json._endpoint }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-Madiad-Source",
              "value": "tpl-schedule-cron-v1"
            },
            {
              "name": "Idempotency-Key",
              "value": "={{ $('Chuẩn bị bài đăng').item.json._id }}"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "neverError": true,
              "fullResponse": true
            }
          }
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($('Chuẩn bị bài đăng').item.json.body) }}"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1160,
        -120
      ],
      "id": "Gọi MADIAD Hub",
      "name": "Gọi MADIAD Hub",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "version": 2
          },
          "conditions": [
            {
              "id": "c",
              "operator": {
                "type": "number",
                "operation": "lt"
              },
              "leftValue": "={{ $json.statusCode }}",
              "rightValue": 300
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        1380,
        -120
      ],
      "id": "Đăng được không?",
      "name": "Đăng được không?"
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "[MADIAD Template] Lịch Đăng Hub"
        },
        "sheetName": {
          "__rl": true,
          "value": "Lịch Đăng",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "row_number": "={{ $('Chuẩn bị bài đăng').item.json._row }}",
            "Trạng Thái": "={{ $json.body?.request_id ? 'Đang Đăng' : 'Đã Đăng' }}",
            "Mã Yêu Cầu": "={{ $json.body?.request_id ?? '' }}",
            "Link Bài Đăng": "={{ Object.entries($json.body?.results ?? {}).map(([k,v]) => k + ': ' + (v?.url ?? (v?.success === false ? ('hỏng, ' + (v?.error ?? '')) : 'đã nhận'))).join('\\n') }}",
            "Mã Bài Đăng": "={{ Object.entries($json.body?.results ?? {}).filter(([,v]) => v?.publish_id).map(([k,v]) => k + '=' + v.publish_id).join(';') }}",
            "Đăng Lúc": "={{ $json.body?.request_id ? '' : $now.setZone('Asia/Ho_Chi_Minh').toFormat('dd/MM/yyyy HH:mm') }}",
            "Lỗi": ""
          },
          "matchingColumns": [
            "row_number"
          ],
          "schema": []
        },
        "options": {
          "dataLocationOnSheet": {
            "values": {
              "rangeDefinition": "specifyRange",
              "headerRow": 1,
              "firstDataRow": 3
            }
          }
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1600,
        -220
      ],
      "id": "Ghi Kết Quả",
      "name": "Ghi Kết Quả",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "version": 2
          },
          "conditions": [
            {
              "id": "c",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.statusCode === 409 && $json.body?.error?.code === 'idempotency_in_progress' }}",
              "rightValue": ""
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        1600,
        -20
      ],
      "id": "Lượt khác đang đăng?",
      "name": "Lượt khác đang đăng?"
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "[MADIAD Template] Lịch Đăng Hub"
        },
        "sheetName": {
          "__rl": true,
          "value": "Lịch Đăng",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "row_number": "={{ $('Chuẩn bị bài đăng').item.json._row }}",
            "Trạng Thái": "Lỗi",
            "Lỗi": "={{ $json.body?.error?.message ?? ('HTTP ' + ($json.statusCode ?? '?')) }}"
          },
          "matchingColumns": [
            "row_number"
          ],
          "schema": []
        },
        "options": {
          "dataLocationOnSheet": {
            "values": {
              "rangeDefinition": "specifyRange",
              "headerRow": 1,
              "firstDataRow": 3
            }
          }
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1820,
        0
      ],
      "id": "Ghi Lỗi",
      "name": "Ghi Lỗi",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "[MADIAD Template] Lịch Đăng Hub"
        },
        "sheetName": {
          "__rl": true,
          "value": "Lịch Đăng",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "row_number": "={{ $json._row }}",
            "Trạng Thái": "Lỗi",
            "Lỗi": "={{ $json._err }}"
          },
          "matchingColumns": [
            "row_number"
          ],
          "schema": []
        },
        "options": {
          "dataLocationOnSheet": {
            "values": {
              "rangeDefinition": "specifyRange",
              "headerRow": 1,
              "firstDataRow": 3
            }
          }
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        940,
        140
      ],
      "id": "Ghi Lỗi Dữ Liệu",
      "name": "Ghi Lỗi Dữ Liệu",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "content": "## MADIAD Hub - Đăng theo lịch từ Google Sheets\n\n**Ba việc trước khi bật:**\n1. Chọn Google Sheet của bạn ở TẤT CẢ các node Google Sheets.\n2. Tạo credential **Header Auth** tên `MADIAD Hub`:\n   Name `Authorization` , Value `Bearer <API key của bạn>`\n   rồi chọn nó ở node gọi API.\n3. Điền `profile_id` ở tab Cấu Hình của sheet.\n\nMã Bài là chìa khoá chống đăng trùng. Sinh từ chính DÒNG (số dòng + nội dung bạn gõ + ngày), TRƯỚC khi gọi API, rồi ghi ngay xuống sheet — nên hai lượt chạy cùng đọc một dòng (webhook và lượt quét) ra cùng một chìa, và Hub chỉ đăng một lần.\n\nhttps://hub.madiad.com/mau",
        "height": 470,
        "width": 440,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -620,
        -200
      ],
      "id": "Hướng dẫn",
      "name": "Hướng dẫn"
    }
  ],
  "connections": {
    "Chạy mỗi 15 phút": {
      "main": [
        [
          {
            "node": "Đọc Cấu Hình",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Đọc Cấu Hình": {
      "main": [
        [
          {
            "node": "Đọc Lịch Đăng",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Đọc Lịch Đăng": {
      "main": [
        [
          {
            "node": "Chuẩn bị bài đăng",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chuẩn bị bài đăng": {
      "main": [
        [
          {
            "node": "Dòng hợp lệ?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Dòng hợp lệ?": {
      "main": [
        [
          {
            "node": "Đánh dấu Đang Đăng",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Ghi Lỗi Dữ Liệu",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Đánh dấu Đang Đăng": {
      "main": [
        [
          {
            "node": "Gọi MADIAD Hub",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gọi MADIAD Hub": {
      "main": [
        [
          {
            "node": "Đăng được không?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Đăng được không?": {
      "main": [
        [
          {
            "node": "Ghi Kết Quả",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Lượt khác đang đăng?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lượt khác đang đăng?": {
      "main": [
        [],
        [
          {
            "node": "Ghi Lỗi",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  }
}