Skip to main content

รับ Callback จาก Chatwoot

ภาพรวม

POST /api/mbox/callback/:oaHash คือขากลับของระบบ live chat กล่าวคือเป็น endpoint ที่ Chatwoot ยิงเข้ามาหาเรา เมื่อมีเหตุการณ์เกิดขึ้นฝั่งเจ้าหน้าที่ ไม่ว่าจะเป็นการพิมพ์ตอบ การเปิดหรือปิดเคส หรือการแก้ไขข้อมูลผู้ติดต่อ เพื่อให้ระบบของเราส่งข้อความกลับไปหาผู้ใช้ LINE และอัปเดตสถานะให้ตรงกัน

Chatwoot ส่ง event เข้ามาจำนวนมากและหลายอย่างไม่เกี่ยวข้องกับเรา เช่น แชทจากช่องทางอื่น หรือข้อความขาเข้าที่เราเป็นคนส่งเข้าไปเอง handler ตัวนี้จึงทำหน้าที่เป็น ตัวกรองล้วน ๆ event ที่ไม่เข้าเกณฑ์จะได้รับคำตอบ {"status":"skipped"} และถูกทิ้งไปโดยไม่ publish

พารามิเตอร์ :oaHash คือ hash ที่ใช้ระบุ LINE OA (คอลัมน์ line_oa.line_oa_hash) ซึ่งไม่ได้ถูกใช้ตัดสินใจอะไรใน handler นี้ แต่ถูกแนบเข้าไปใน payload เป็นฟิลด์ _oaHash เพื่อให้ worker-go นำไป resolve ต่อ

จุดที่ต่างจาก endpoint ของ LINE คือ endpoint นี้ ไม่ใช่ fire-and-forget แต่รอให้ publish สำเร็จก่อนจึงตอบกลับ เพื่อให้ Chatwoot สามารถ retry ได้หากระบบเรามีปัญหา

Business Flow

  1. อ่าน :oaHash จาก path
  2. อ่าน body แล้ว parse เป็น JSON หากอ่านไม่ได้หรือไม่ใช่ JSON จะตอบ 200 พร้อม {"status":"skipped"}
  3. กรองตามค่า body.event โดยยอมรับเพียง 5 ประเภทดังนี้
eventเงื่อนไขที่ต้องผ่าน
message_createdchannel ต้องเป็น Channel::Line และ message_type ต้องเป็น "outgoing" หรือ 1 ซึ่งหมายถึงข้อความที่เจ้าหน้าที่ส่งออก
conversation_createdchannel ต้องเป็น Channel::Line โดยอ่านจาก body.channel ก่อน หากว่างจึงดู conversation.channel
conversation_updatedchannel จาก body.channel ต้องเป็น Channel::Line
conversation_status_changedchannel ต้องเป็น Channel::Line และ status ต้องเป็น resolved หรือ open
contact_updatedผ่านทุกกรณี ใช้สำหรับ sync attribute ของผู้ติดต่อ

event ประเภทอื่นทั้งหมดจะถูกตอบกลับเป็น skipped

:::note รายละเอียด parity การตรวจ channel เขียนในรูปแบบ "ไม่ว่าง และไม่เท่ากับ Channel::Line" ซึ่งหมายความว่า หากไม่มีฟิลด์ channel เลยจะถือว่าผ่าน ตามพฤติกรรมของต้นฉบับ NestJS ส่วน message_type รองรับทั้ง string "outgoing" และตัวเลข 1 ซึ่งใน JSON จะถูกอ่านเป็น float64 :::

  1. เมื่อผ่านตัวกรองแล้ว ระบบจะแนบฟิลด์ _oaHash เข้าไปใน body
  2. publish body ทั้งก้อนเป็น bare JSON ลงคิว mbox_callback ด้วย timeout 5 วินาที โดยรอผลจริง
  3. หาก publish สำเร็จจะ log info Mbox callback received และตอบ 200 พร้อม {"status":"ok"} หากล้มเหลวจะ log error และตอบ 500 พร้อม {"status":"error"}
  4. หากไม่ได้ตั้งค่า AMQP ไว้ เช่น boot แบบไม่มี backend ระบบจะข้ามการ publish แต่ยังตอบกลับเป็น ok

log ทุกบรรทัดจะแนบค่า event และ conversationId โดยอ่านจาก conversation.id หากไม่มีจึงใช้ body.id แทน

ไฟล์และฟังก์ชันหลัก

MethodRouteAuthHandler
POST/api/mbox/callback/:oaHashไม่มีhandleCallback(deps)

โค้ดอยู่ที่ internal/mbox/handler.go โดยแพ็กเกจนี้มีไฟล์เดียว ไม่มี service หรือ repository layer แยก

ฟังก์ชันหน้าที่
Register(r, deps)mount route โดยไม่ครอบด้วย api-key guard
handleCallback(deps) gin.HandlerFuncดำเนิน flow ทั้งหมดข้างต้น
accept(body map[string]any) boolตัวกรอง event ทั้ง 5 ประเภท
isOutgoing(v any) boolรองรับทั้ง "outgoing", float64(1) และ json.Number("1")
nested, str, conversationID, logInfo, logErrorฟังก์ชันช่วย
ค่าคงที่ channelLinediscriminator ของ Chatwoot ที่มีค่า Channel::Line

จุดเชื่อมต่อกับ Service อื่น

  • RabbitMQ — คิว mbox_callback ที่กำหนดผ่าน RABBITMQ_QUEUE_MBOX_CALLBACK (ค่าเริ่มต้น mbox_callback) ส่งเป็น bare JSON บน exchange line_exchange
  • worker-go — เป็น consumer ที่นำ _oaHash ไปค้นหา OA แล้วส่งข้อความของเจ้าหน้าที่ กลับผ่าน LINE Messaging API รวมถึงอัปเดตหรือปิด agent_mode key เมื่อเคสถูก resolve
  • Chatwoot — ระบบต้นทางที่ยิงเข้ามา โดย URL ของ callback ถูกตั้งค่าไว้ที่ฝั่ง Chatwoot
  • ไม่มีการใช้ฐานข้อมูลและ Redis ในเส้นทางนี้เลย
  • ขาไปจาก LINE ไปยัง Chatwoot อยู่ที่ ส่งต่อให้เจ้าหน้าที่
  • ความเสี่ยงที่ควรทราบ — endpoint นี้เปิดสาธารณะโดยไม่มี auth และไม่ได้ verify ว่า request มาจาก Chatwoot จริง การป้องกันที่มีอยู่คือ oaHash ที่เดาได้ยาก และ rate limit ต่อ IP เท่านั้น