[How to] สร้าง Messenger Chatbot แบบ Serverless ด้วย Google Firebase

Nontapat Piyamongkol
5 min readMay 27, 2017

--

เกริ่นเบื้องต้นเกี่ยวกับ Messenger

Messenger Platform เป็นระบบของ Facebook ที่เปิดให้นักพัฒนาเข้าไปสร้างบอทเพื่อโต้ตอบกับผู้คนที่เข้ามาสอบถามหรือพูดคุยบน Facebook Page ได้ โดยการทำงานของระบบมันคือการที่แอปเราจะมี URL หนึ่งที่เป็น webhook สำหรับการคอยตรวจ event ที่ทาง Messenger ส่งมาให้ แล้วรับข้อความไปประมวลผลต่อ

ถ้าใครเคยลองทำตามไกด์ก็จะเจอว่า Facebook แนะนำให้เราสร้างโปรเจค Node.js + Express.js ขึ้นมา หรือไป Fork มาจาก Github ก็ได้แล้วก็เซ็ต webhook ด้วย Express.js พร้อมกับตั้ง Verify Token เพื่อให้ตรงกับการยืนยัน webhook บนตัวแอป

const app = require('express');app.get('/webhook', function(req, res) {  if (req.query['hub.mode'] === 'subscribe' &&
req.query['hub.verify_token'] === <VERIFY_TOKEN>) {
console.log("Validating webhook");
res.status(200).send(req.query['hub.challenge']);
} else { console.error("Failed validation. Make sure the validation
tokens match.");
res.sendStatus(403);

}
});

นี่คือโค้ดจากหน้าไกด์ของ Facebook Developer เราต้องแก้ไข <VERIFY_TOKEN> ให้เป็นข้อความของเรา เช่น ผมจะตั้งเป็น Congrat_for_your_Harvard_degree_Mark พอไปที่หน้าตั้งค่าของแอปใน Facebook เราก็เอา webhook URL เป็น https://ชื่อเว็บไซต์/webhook แล้วก็ใส่ Verify Token เป็น Congrat_for_your_Harvard_degree_Mark ตามที่เราตั้งไว้นั่นเอง พอกด Verify ระบบของ Facebook ก็จะส่ง HTTP GET ไปยัง webhook URL ที่เราตั้งไว้ พร้อมกับ Verify Token ที่จะเอาไปตรวจสอบว่าตรงกันหรือเปล่า ถ้าตรงก็คือผ่าน

หลังจากการ Verify ผ่าน ระบบหลังบ้านเราก็พร้อมที่จะรับข้อความที่ Messenger ส่งต่อมาให้เรา เอาไปประมวลผลหรือเลือกการโต้ตอบกลับได้นั่นเอง

การตั้ง path สำหรับ webhook นั้นพูดอีกอย่างก็คือเราต้องมีเซิฟเวอร์อยู่ตัวนึง ที่เปิดรอรับสัญญาณจาก Messenger ว่ามีข้อความเข้ามาแล้ว อย่างระบบก่อนหน้านี้ที่ผมทำนั้นก็ใช้วิธีตั้งเซิฟเวอร์บน Heroku เอา และด้วยความอยากลองเล่นของใหม่อย่าง Firebase ก็เลยใช้ Realtime Database ของ Firebase เก็บข้อมูล ซึ่งก็เวิร์คครับ ใช้งานได้ดีทีเดียว การเชื่อมต่อไม่มีปัญหา

แต่พอใช้มาสักพักก็คิดขึ้นมาว่า ถ้าจะรับโหลดเยอะๆ หรือต้องการ Scale เพิ่มขึ้น มันก็กลายเป็นต้องจ่ายเงินเพิ่มทั้งฝั่ง Heroku และ Firebase เลยนี่หว่า ก็เลยได้ไอเดียว่า ย้ายมาบน Firebase ให้หมดเลยละกัน เพราะเราศรัทธาในระบบ Cloud ของ Google ที่มัน Scale ได้ตามการใช้งาน

Firebase ทำระบบหลังบ้านได้ด้วยหรอ ?

ทำได้ครับ เพราะเมื่อช่วงมีนาคมที่ผ่านมา ทาง Google ได้เปิดบริการของ Firebase เพิ่มอีกตัวนั่นก็คือ Firebase Cloud Function ที่เปิดให้เราเขียนโค้ดบน Node.js แล้วจับ Deploy ขึ้นไปเป็นฟังก์ชันได้ โดยจะทำงานเมื่อเกิด Event ต่างๆ เช่น HTTPS Request, มีการเปลี่ยนแปลงของข้อมูล Database หรือ Storage ก็ได้ สำหรับรายละเอียดเชิงลึกแนะนำบล็อกของพี่ตี๋ Jirawatee 🔥 GDE Firebase ประเทศไทย ลองอ่านดูได้ครับ

เมื่อเห็นว่ามี Cloud Functions ให้ใช้ผมก็หมดห่วงเรื่องระบบหลังบ้านแล้ว เพราะทำได้แน่ๆ แต่หลังจากศึกษาสักพักผมก็พบว่า …

อ้าวเฮ้ย Firebase แม่งเอา Express ขึ้นไปรันไม่ได้ว่ะ

จากเดิมที่เราต้องตั้ง webhook URL เพื่อให้ทาง Messenger ส่งข้อความมาให้เรา แต่ไม่มี Express ให้เรากำหนด URL แล้วเราจะทำยังไง ผมก็คิดอยู่พักใหญ่ ระหว่างนั้นก็เลยลองเขียนฟังก์ชันอื่นๆ ขึ้นไปรันเล่นด้วย เพื่อให้ชินมือกับการทำ Cloud Functions แล้วก็เอะใจขึ้นมาว่า เวลาที่เราเขียนฟังก์ชันสำหรับ Deploy ขึ้นไปเป็น Cloud Functions มันเป็นแบบนี้

const functions = require('firebase-functions')exports.helloFirebase = functions.https.onRequest((req, res) => {
res.send("Hello Firebase!")
})

แล้วพอขึ้นไปเรียบร้อย เราก็เรียกมันผ่าน

https://us-central1-[ชื่อโปรเจคของเรา].cloudfunctions.net/helloFirebase

และโครงสร้างฟังก์ชันของมันแท้จริงแล้วก็คือ Express.js มีการส่ง parameter ทั้ง request, response มาให้ด้วย แบบนี้เราก็เอา request.method มาอ่าน HTTP method ได้ด้วยนี่หว่า ก็เลยได้ไอเดียเขียนฟังก์ชันขึ้นมาอีกตัวหนึ่ง หน้าตาแบบนี้

exports.webhook = functions.https.onRequest((req, res) => { if(req.method == "GET") {

if (req.query['hub.mode'] === 'subscribe' &&
req.query['hub.verify_token'] === "firebaseWebhook") {
console.log("Validating webhook");
res.status(200).send(req.query['hub.challenge']);
}
else {
console.error("Failed validation. Make sure the validation
tokens match.");
res.sendStatus(403);
} }})

พอเขียนเสร็จเรียบร้อยก็ส่ง Deploy ขึ้นไป เราก็จะได้ฟังก์ชันใหม่มาเป็น URL นี้

https://us-central1-[ชื่อโปรเจคของเรา].cloudfunctions.net/webhook

ได้มาแบบนี้ก็รีบเอาไปลองกับ Facebook เลยครับ ก็โยน URL นี้ลงไปในส่วนตั้งค่า webhook แล้วก็ใส่ Verify Token เป็น firebaseWebhook ตามที่กำหนดไว้ในโค้ด

แอบรู้สึกว่ามักง่ายในการเลือกภาพไปหน่อย แหะๆ

กด Verify and Save ไปเลย … เราก็จะพบว่า … มันผ่านครับ!

เท่ากับว่าตอนนี้เราใช้ Firebase Cloud Functions เป็น webhook ได้แล้วนั่นเอง แต่เพื่อให้มั่นใจ เราจะมาเขียนต่อเพื่อดูว่ามันรับข้อความจาก Messenger เข้ามาได้จริงๆ ซึ่งในส่วนการรับข้อความนั้นทาง Messenger จะส่ง request มาเป็น HTTP POST นั่นเอง เราก็เขียนเพิ่มจากฟังก์ชัน webhook ตะกี้นิดหน่อยให้ออกมาเป็นประมาณนี้

exports.webhook = functions.https.onRequest((req, res) => {  if(request.method == "GET") {
// ส่วน GET ก็ใช้โค้ดแบบเดิมไปครับ
}
else if(req.method == "POST") { var data = req.body;
if (data.object === 'page') {
data.entry.forEach(entry => { var pageID = entry.id;
var timeOfEvent = entry.time;
console.log(`entry : ${JSON.stringify(entry)}`) entry.messaging.forEach(event => {
if (event.message) {
receivedMessage(event)
} else {
console.log("Webhook received unknown event: ", event)
}
})

})
res.sendStatus(200) } }})

โค้ดที่เพิ่มมาในส่วนของการรับ request ที่เป็น HTTP POST นั้นผมก็ใช้โค้ดจากไกด์ของ Facebook เลยครับ การทำงานของมันก็คือ เมื่อมีการ POST เข้ามาที่ webhook มันก็จะตรวจสอบว่าข้อมูลที่ส่งมานั้นเป็นข้อความหรือเปล่า ถ้าใช่ก็จะไปเรียกฟังก์ชันที่ชื่อว่า receivedMessage ซึ่งแน่นอนว่า เราก็ต้องไปเขียนเพิ่มครับ แต่ถ้าใคร Fork มาจาก Github ก็น่าจะมีให้อยู่แล้วครับ

function receivedMessage(event) {  let senderID = event.sender.id
let recipientID = event.recipient.id
let timeOfMessage = event.timestamp
let message = event.message
//ถ้าข้อความมาแล้ว log ตรงนี้จะเห็นข้อความเลยครับ
console.log("Received message for user %d and page %d at %d with
message:", senderID, recipientID, timeOfMessage)
console.log(JSON.stringify(message))
let messageId = message.mid
let messageText = message.text
let messageAttachments = message.attachments
if (messageText) { //ส่วนนี้ใช้ Switch case มาทำ rule-base คือดักคำมาทำงานแตกต่างกันไป
//เรียกได้ว่าเป็นวิธีที่ basic และง่ายสุดในการทำบอทก็ว่าได้ 555
switch (messageText.toLowerCase()) {
case 'hello':
greeting(senderID)
break;
default:
sendTextMessage(senderID, messageText)
} } else if (messageAttachments) {
sendTextMessage(senderID, "Message with attachment received");
}
}

ผมจะขอละฟังก์ชัน greeting เอาไว้ก่อนนะครับ เราจะไปสนใจใน default case ก่อน สิ่งที่ต้องทำต่อไปนั่นก็คือฟังก์ชัน sendTextMessage เมื่อเขียนตามไกด์ก็จะได้มาหน้าตาประมาณนี้ครับ

function sendTextMessage(recipientId, messageText) {  //จัดข้อความที่จะส่งกลับในรูปแบบ object ตามที่ Messenger กำหนด  let messageData = {
recipient: {
id: recipientId
},
message: {
text: messageText//,
//metadata: "DEVELOPER_DEFINED_METADATA"
}
}
callSendAPI(messageData)}

ข้อความที่เราจะส่งกลับไปนั้นไม่ใช่ว่าโยน text ส่งกลับไปนะครับ ต้องจัดให้ถูก format ตามที่ Messenger กำหนดก่อนด้วย รายละเอียดสามารถอ่านเพิ่มได้ในหน้าเกี่ยวกับ Send API ของ Messenger ครับ

พอเราเขียนตรงนี้เสร็จแล้ว ขั้นต่อไปก็คือเขียน callSendAPI ให้ยิง Request ไปยัง API ของ Messenger นั่นเอง ถ้าตามไกด์เค้าจะแนะนำให้ใช้ module “Request” แต่ด้วยความขี้เกียจตั้งค่าเกี่ยวกับ json ผมจึงเลือกใช้ axios แทนครับ … เอาจริงๆ แล้วผมชอบ node-fetch มากกว่านะ แต่ Cloud Functions มันไม่รู้จัก… Deploy ไปแล้วใช้ไม่ได้ ;(

หน้าตาของ callSendAPI ของผมจึงออกมาเป็นแบบนี้

const axios = require('axios')function callSendAPI(messageData) {  console.log(`message data : ${JSON.stringify(messageData)}`);  axios({    method: 'POST',
url: 'https://graph.facebook.com/v2.6/me/messages',
params: {
'access_token': <FACEBOOK_PAGETOKEN>
},
data: messageData
})
.then(res => {
if (res.status == 200) { let body = res.data
let recipientId = body.recipient_id;
let messageId = body.message_id;
if (messageId) { console.log("Successfully sent message with id %s
to recipient %s", messageId, recipientId);
} else { console.log("Successfully called Send API for recipient %s",
recipientId);
} }
else {
console.error("Failed calling Send API", res.status,
res.statusText, res.data.error);
}
})
.catch(error => {
console.log(`error : ${error}`)
console.log(`axios send message failed`);
})
}

ให้เราใส่ค่า Page Token ของเพจเรา โดยค่านี้เอาได้จาก หน้าแอป Facebook → Messenger → Token Generation แล้วเลือกเพจของเรา มันจะก็สร้าง Token ขึ้นมาให้ ก็เอามาใส่แทนตรง <FACEBOOK_PAGETOKEN> ในโค้ดครับ

แล้วก็อย่าลืมอีกอย่างครับ เลื่อนลงมาอีกนิดนึงในหน้าเดียวกันจะมีส่วนการตั้งค่าของ Webhooks ในส่วนนี้ให้เราเลือกเพจของเราแล้วกด Subscribe ไปด้วยครับ จะได้ map ระบบหลังบ้านเราเข้ากับ Facebook Page ได้ถูกต้อง

เอาล่ะ มาถึงจุดนี้แล้วก็ลอง Deploy ฟังก์ชันของเราขึ้นไปดูครับ พวกฟังก์ชันยิบย่อยที่ถูกเรียกด้วยฟังก์ชันที่เราสั่ง exports ก็จะถูกจับขึ้นไปด้วยครับ รวมถึงการ require ไฟล์ต่างๆ ก็เช่นกัน ดังนั้นไม่ต้องห่วงว่าไฟล์จะขึ้นไปไม่ครบ

เมื่อ Deploy เรียบร้อยแล้วก็ลองไปที่ Facebook Page ที่เราจับผูก Subscribe กับระบบหลังบ้านบน Firebase ไว้ แล้วก็เปิดช่อง Message เพื่อส่งข้อความเข้าไปที่เพจครับ ลองพิมพ์ข้อความอะไรไปก็ได้ (อย่าไปตรงกับ ‘hello’ ใน case ที่เรายังไม่กันไว้เป็นโอเคครับ ฮ่าๆ) ถ้าทุกอย่างราบรื่น ทำงานได้ด้วยดี ระบบหลังบ้านบน Firebase ของเราก็จะส่งข้อความกลับมา …

รึเปล่า? …

ไม่ครับ ไม่เสมอไป

อ้าว เพราะอะไร? เพราะว่าการที่ Firebase นั้นจะไม่ยอมเปิดให้เรา request API ภายนอกครับ จนกว่าเราจะเปย์มัน ถ้าใช้เป็นแพคเกจฟรีมันจะยอมให้ใช้ได้แต่บริการของ Google เท่านั้นครับ อ่านเพิ่มเติมได้ที่ Firebase Pricing ครับ แต่ผมแนะนำว่าสมัครแบบ Blaze Plan ไปเลยดีกว่าครับ เพราะว่ามันจ่ายตามการใช้งาน ถ้าเดือนไหนใช้งานน้อยก็จ่ายนิดเดียว ไม่โดน fix เป็น $25 ทุกเดือนๆ เหมือน Flame Plan

เอาล่ะ ถ้าใครปาเงินใส่มันเรียบร้อย (แค่ผูกบัตรเครดิต/เดบิตไปเท่านั้นแหละครับ) แล้วก็กลับมาลองกันใหม่อีกรอบ ผมลองส่งคำว่า “Firebase ❤ Messenger” ไปที่เพจของผมดู แล้วสิ่งบอทตอบกลับมานั่นก็คือ …

บอทจะทำหน้าที่เป็น echo bot ที่ตอบคำที่เราพิมพ์ไปกลับมานั่นเองครับ เป็นไปตามกระบวนการที่เราตั้งไว้ใน default case ที่รับ messageText ส่งกลับไปยังเจ้าของข้อความ

สำหรับบล็อกนี้ผมคงจบแต่เพียงเท่านี้ครับ เชื่อว่าถ้าทำสำเร็จตามมาถึงตรงนี้ได้ การดัดแปลงต่อ เขียนการทำงานเพิ่มเติมก็ไม่ยากเท่าไรแล้วครับ Document ของทาง Facebook ก็โอเคอยู่ สามารถศึกษาเพิ่มเติมได้ ผมว่า Chatbot นั้นเป็นอีกกระแสหนึ่งที่น่าสนใจครับ และน่าจะกำลังเติบโตอยู่เรื่อยๆ โดยเฉพาะในภาคธุรกิจที่อาจจะนำทดแทนงาน routine ซ้ำซากเพื่อลดแรงงานมนุษย์ลง

การที่เราเอา Firebase มาพัฒนาเป็นระบบของบอทได้ก็เป็นการลดภาระการเซ็ตอัพเซิฟเวอร์และระบบหลังบ้านไปพอตัวเลย สำหรับนักพัฒนาหรือใครก็ตามที่สนใจก็สามารถเริ่มต้นได้อย่างรวดเร็วมาก อาจจะลองทำเป็นผู้ช่วยส่วนตัวเล่นๆ ดูก่อนก็ยังได้ เช่น request API ของเว็บพยากรณ์อากาศมารายงานสภาพอากาศทุกๆ เช้า กลางวัน เย็น หรืออาจจะพัฒนาเอา Machine Learning มาเชื่อมกับหลังบ้านเพื่อตอบโต้กับคนก็ยังได้ครับ

ถ้ามีข้อสงสัยตรงไหน สามารถคอมเมนท์ถามมาได้เลยนะครับ ถ้าตรงไหนผมช่วยได้จะมาตามตอบอีกทีหนึ่ง :)

--

--

Nontapat Piyamongkol

Junior Developer @ TakeMeTour | Technology Enthusiast | Blogger @ Droidsans