รับ 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
- อ่าน
:oaHashจาก path - อ่าน body แล้ว parse เป็น JSON หากอ่านไม่ได้หรือไม่ใช่ JSON จะตอบ
200พร้อม{"status":"skipped"} - กรองตามค่า
body.eventโดยยอมรับเพียง 5 ประเภทดังนี้
| event | เงื่อนไขที่ต้องผ่าน |
|---|---|
message_created | channel ต้องเป็น Channel::Line และ message_type ต้องเป็น "outgoing" หรือ 1 ซึ่งหมายถึงข้อความที่เจ้าหน้าที่ส่งออก |
conversation_created | channel ต้องเป็น Channel::Line โดยอ่านจาก body.channel ก่อน หากว่างจึงดู conversation.channel |
conversation_updated | channel จาก body.channel ต้องเป็น Channel::Line |
conversation_status_changed | channel ต้องเป็น 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
:::
- เมื่อผ่านตัวกรองแล้ว ระบบจะแนบฟิลด์
_oaHashเข้าไปใน body - publish body ทั้งก้อนเป็น bare JSON ลงคิว
mbox_callbackด้วย timeout 5 วินาที โดยรอผลจริง - หาก publish สำเร็จจะ log info
Mbox callback receivedและตอบ200พร้อม{"status":"ok"}หากล้มเหลวจะ log error และตอบ500พร้อม{"status":"error"} - หากไม่ได้ตั้งค่า AMQP ไว้ เช่น boot แบบไม่มี backend ระบบจะข้ามการ publish แต่ยังตอบกลับเป็น ok
log ทุกบรรทัดจะแนบค่า event และ conversationId โดยอ่านจาก conversation.id
หากไม่มีจึงใช้ body.id แทน
ไฟล์และฟังก์ชันหลัก
| Method | Route | Auth | Handler |
|---|---|---|---|
| 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 | ฟังก์ชันช่วย |
ค่าคงที่ channelLine | discriminator ของ Chatwoot ที่มีค่า Channel::Line |
จุดเชื่อมต่อกับ Service อื่น
- RabbitMQ — คิว
mbox_callbackที่กำหนดผ่านRABBITMQ_QUEUE_MBOX_CALLBACK(ค่าเริ่มต้นmbox_callback) ส่งเป็น bare JSON บน exchangeline_exchange - worker-go — เป็น consumer ที่นำ
_oaHashไปค้นหา OA แล้วส่งข้อความของเจ้าหน้าที่ กลับผ่าน LINE Messaging API รวมถึงอัปเดตหรือปิดagent_modekey เมื่อเคสถูก resolve - Chatwoot — ระบบต้นทางที่ยิงเข้ามา โดย URL ของ callback ถูกตั้งค่าไว้ที่ฝั่ง Chatwoot
- ไม่มีการใช้ฐานข้อมูลและ Redis ในเส้นทางนี้เลย
- ขาไปจาก LINE ไปยัง Chatwoot อยู่ที่ ส่งต่อให้เจ้าหน้าที่
- ความเสี่ยงที่ควรทราบ — endpoint นี้เปิดสาธารณะโดยไม่มี auth และไม่ได้ verify
ว่า request มาจาก Chatwoot จริง การป้องกันที่มีอยู่คือ
oaHashที่เดาได้ยาก และ rate limit ต่อ IP เท่านั้น