{
 "id": "RhR4Chat00000001",
 "name": "N5 ช่องแชต LINE/Facebook (ลายเซ็น กันซ้ำ ตอบตามคำสำคัญ)",
 "active": false,
 "nodes": [
  {
   "id": "r4n1",
   "name": "Webhook",
   "type": "n8n-nodes-base.webhook",
   "typeVersion": 2,
   "position": [
    0,
    300
   ],
   "parameters": {
    "httpMethod": "POST",
    "path": "r4-chat",
    "responseMode": "responseNode",
    "options": {
     "rawBody": true
    }
   },
   "webhookId": "a3fcb004-3127-1260-0ea4-8b1b373d273c"
  },
  {
   "id": "r4n2",
   "name": "Verify + normalise",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    220,
    300
   ],
   "parameters": {
    "jsCode": "// N5 ช่องแชต LINE / Facebook → n8n · ตรวจลายเซ็น แปลงเหตุการณ์เป็นรูปเดียวกัน กันส่งซ้ำ เลือกคำตอบจากคำสำคัญ\n// ใช้ได้ทั้งใน Code node (n8n) และใน node:test (ไฟล์เดียวกัน) · ไม่ส่งข้อความจริง: ผลลัพธ์คือ payload ที่ \"จะ\" ส่ง\nfunction chatIn(req, ctx) {\n  const crypto = ctx.crypto;\n  const h = Object.fromEntries(Object.entries(req.headers || {}).map(([k, v]) => [k.toLowerCase(), v]));\n  const raw = req.rawBody || \"\";\n  const eq = (a, b) => { const x = Buffer.from(String(a)), y = Buffer.from(String(b)); return x.length === y.length && crypto.timingSafeEqual(x, y); };\n  let channel;\n  // a missing or empty channel secret signs with an empty HMAC key that anyone can compute: refuse, never \"verify\"\n  const unset = (s) => typeof s !== \"string\" || !s;\n  if (h[\"x-line-signature\"] !== undefined && unset(ctx.lineSecret)) return { code: 500, response: { error: \"LINE secret not set\" }, events: [] };\n  if (h[\"x-hub-signature-256\"] !== undefined && h[\"x-line-signature\"] === undefined && unset(ctx.fbSecret)) return { code: 500, response: { error: \"Facebook secret not set\" }, events: [] };\n  if (h[\"x-line-signature\"] !== undefined) {\n    const want = crypto.createHmac(\"sha256\", ctx.lineSecret).update(raw).digest(\"base64\");\n    if (!eq(h[\"x-line-signature\"], want)) return { code: 401, response: { error: \"bad signature\" }, events: [] };\n    channel = \"line\";\n  } else if (h[\"x-hub-signature-256\"] !== undefined) {\n    const want = \"sha256=\" + crypto.createHmac(\"sha256\", ctx.fbSecret).update(raw).digest(\"hex\");\n    if (!eq(h[\"x-hub-signature-256\"], want)) return { code: 401, response: { error: \"bad signature\" }, events: [] };\n    channel = \"facebook\";\n  } else return { code: 401, response: { error: \"no signature\" }, events: [] };\n  let body;\n  try { body = JSON.parse(raw); } catch (e) { return { code: 400, response: { error: \"bad json\" }, events: [] }; }\n  // ผู้ใช้เก็บเป็นค่าแฮชสั้น ไม่เก็บ userId/PSID ตรง ๆ (PDPA: เก็บเท่าที่จำเป็น)\n  const ref = (id) => crypto.createHash(\"sha256\").update(ctx.salt + \":\" + id).digest(\"hex\").slice(0, 12);\n  const seen = ctx.seen; // Set ของ event id ที่ทำไปแล้ว (ใน n8n = $getWorkflowStaticData('global'))\n  const events = [];\n  const raw_ev = channel === \"line\"\n    ? (body.events || []).map((e) => ({ id: e.webhookEventId, redelivery: !!(e.deliveryContext && e.deliveryContext.isRedelivery), user: e.source && e.source.userId, type: e.type === \"message\" && e.message && e.message.type === \"text\" ? \"text\" : e.type, text: e.message && e.message.text, replyToken: e.replyToken }))\n    : (body.entry || []).flatMap((en) => (en.messaging || []).map((m) => ({ id: m.message && m.message.mid, redelivery: false, user: m.sender && m.sender.id, type: m.message && m.message.text !== undefined ? \"text\" : m.message && m.message.is_echo ? \"echo\" : \"other\", text: m.message && m.message.text, echo: !!(m.message && m.message.is_echo) })));\n  for (const e of raw_ev) {\n    if (!e.id || seen.has(e.id)) continue; // ส่งซ้ำ (redelivery) ⇒ ข้าม ไม่ตอบซ้ำ\n    seen.add(e.id);\n    if (e.echo || e.type !== \"text\") { events.push({ channel, id: e.id, user: e.user ? ref(e.user) : \"\", kind: \"ignored\", reply: null }); continue; }\n    events.push({ channel, id: e.id, user: ref(e.user), kind: \"text\", text: String(e.text).slice(0, 500), reply: pick(String(e.text)), replyToken: e.replyToken || null });\n  }\n  return { code: 200, response: { ok: true }, events };\n}\n\nfunction pick(t) {\n  const s = t.toLowerCase();\n  if (/(ราคา|เท่าไร|กี่บาท|price)/.test(s)) return \"ราคาอยู่ในเมนู \\\"สินค้า\\\" ครับ ทีมงานจะตอบรายละเอียดภายในเวลาทำการ\";\n  if (/(เปิด|ปิด|กี่โมง|เวลา)/.test(s)) return \"ร้านเปิด 09:00–18:00 ทุกวันครับ\";\n  if (/(พัสดุ|ส่งของ|tracking|เลขพัสดุ)/.test(s)) return \"ส่งเลขออเดอร์มาได้เลยครับ ทีมงานจะเช็กสถานะให้\";\n  return null; // ไม่เข้าคำสำคัญ ⇒ ไม่ตอบอัตโนมัติ ส่งให้คนตอบ\n}\n\n\n\nconst it = $input.first();\nconst raw = it.binary && it.binary.data ? Buffer.from(it.binary.data.data, \"base64\").toString(\"utf8\") : JSON.stringify(it.json.body);\nconst st = $getWorkflowStaticData(\"global\"); st.seen = st.seen || [];\nconst seen = new Set(st.seen);\nconst r = chatIn({ rawBody: raw, headers: it.json.headers }, { crypto: require(\"crypto\"), lineSecret: $env.LINE_CHANNEL_SECRET, fbSecret: $env.FB_APP_SECRET, salt: $env.USER_HASH_SALT, seen });\nst.seen = [...seen].slice(-500);\nreturn [{ json: r }];"
   }
  },
  {
   "id": "r4n3",
   "name": "Respond 200/401",
   "type": "n8n-nodes-base.respondToWebhook",
   "typeVersion": 1.1,
   "position": [
    440,
    200
   ],
   "parameters": {
    "respondWith": "json",
    "responseBody": "={{ JSON.stringify($json.response) }}",
    "options": {
     "responseCode": "={{ $json.code }}"
    }
   }
  },
  {
   "id": "r4n4",
   "name": "One item per reply",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    440,
    400
   ],
   "parameters": {
    "jsCode": "return $input.first().json.events.filter(e => e.reply).map(e => ({ json: e }));"
   }
  },
  {
   "id": "r4n5",
   "name": "Send reply (buyer channel API)",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    660,
    400
   ],
   "parameters": {
    "method": "POST",
    "url": "={{ $env.BUYER_API_BASE }}/reply",
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify({ channel: $json.channel, replyToken: $json.replyToken, user: $json.user, text: $json.reply }) }}",
    "options": {
     "timeout": 10000
    }
   },
   "onError": "continueErrorOutput",
   "retryOnFail": true,
   "maxTries": 3
  },
  {
   "id": "r4n6",
   "name": "Log to sheet",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    880,
    400
   ],
   "parameters": {
    "method": "POST",
    "url": "={{ $env.BUYER_API_BASE }}/sheets/append",
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify({ sheet: 'แชต', row: [$json.channel, $json.user, $json.reply ? 'ตอบอัตโนมัติ' : 'รอคนตอบ'] }) }}",
    "options": {
     "timeout": 10000
    }
   },
   "onError": "continueErrorOutput",
   "retryOnFail": true,
   "maxTries": 3
  }
 ],
 "connections": {
  "Webhook": {
   "main": [
    [
     {
      "node": "Verify + normalise",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Verify + normalise": {
   "main": [
    [
     {
      "node": "Respond 200/401",
      "type": "main",
      "index": 0
     },
     {
      "node": "One item per reply",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "One item per reply": {
   "main": [
    [
     {
      "node": "Send reply (buyer channel API)",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Send reply (buyer channel API)": {
   "main": [
    [
     {
      "node": "Log to sheet",
      "type": "main",
      "index": 0
     }
    ]
   ]
  }
 },
 "settings": {
  "executionOrder": "v1",
  "saveDataErrorExecution": "all"
 },
 "pinData": {}
}