ใน Workshop นี้ เราจะรับบทเป็นเจ้าของร้านค้า สร้างบอตหนึ่งตัวสำหรับสมาชิกเพื่อใช้ในการเช็คคะแนนสะสม และอีกหนึ่งตัวสำหรับร้านค้าเพื่อใช้ในการจัดการสมาชิก
- ไปที่ https://codesandbox.io/p/devbox/e-membership-template-th-rlh7h5
- กดปุ่ม "Fork" ที่มุมขวาบนของหน้า
หลังจาก Fork แล้ว โปรเจคที่เห็นในจอจะเปลี่ยนเป็นโปรเจคที่เป็นของคุณเอง และสามารถแก้ไขได้ตามต้องการ
- กดปุ่ม "Share" ที่มุมขวาบนของหน้า
- กดปุ่ม "Move out of Drafts"
- ที่ "Change permissions" ให้เลือก "Unlisted" เพื่อทำให้โปรเจคสามารถเข้าถึงได้
- กดที่กล่องสี่เหลี่ยมที่มุมซ้ายบน แล้วกด "Restart Devbox"
โปรเจคที่เพิ่งก็อปไปประกอบไปด้วยไฟล์สำคัญ 4 ไฟล์ คือ
index.jsไฟล์หลักของโปรเจค โปรแกรมจะเริ่มทำงานจากไฟล์นี้persists.jsรวมฟังก์ชันต่างๆ ที่เกี่ยวกับการติดต่อกับฐานข้อมูล Database เอาไว้ในนั้นmember.jsโค้ดของบอตตัวที่หนึ่ง บอตฝั่งสมาชิกmerchant.jsโค้ดของบอตตัวที่สอง บอตฝั่งร้านค้า
นอกจากนั้น เรายังมีฟังก์ชันที่เอาไว้ใช้ในการส่งข้อความได้แก่
pushToMemberใช้สำหรับการส่งข้อความแบบpushไปยังสมาชิกreplyToMemberใช้สำหรับการส่งข้อความตอบกลับไปยังสมาชิกreplyToMerchantใช้สำหรับการส่งข้อความตอบกลับไปยังร้านค้า
- สร้างบอตใหม่ตามขั้นตอนในไฟล์นี้
- หลังจากสร้างบอตเสร็จแล้ว ไปที่หน้า "Basic settings" ของบอต
- คัดลอก Channel secret ไปแทนที่คำว่า
MEMBER_CHANNEL_SECRETในไฟล์member.js - ไปที่หน้า "Messaging API" ของบอต
- คัดลอก Channel access token ไปแทนที่คำว่า
MEMBER_CHANNEL_ACCESS_TOKENในไฟล์เดียวกัน - ใน Codesandbox, กด Save (
Ctrl+S), โปรแกรมจะเริ่มใหม่โดยอัตโนมัติ - ใน Codesandbox จอครึ่งขวาจะเป็น Browser ที่มีลิงค์อยู่ด้านบน ให้ Copy ลิงค์นั้นไปวางในหน้า "Messaging API" ของบอต ในช่อง "Webhook URL" แล้วต่อท้ายด้วย
/webhooks/member
- กด "Update" แล้วกด "Verify" เพื่อทดสอบการเชื่อมต่อระหว่าง LINE กับบอต; ถ้าทดสอบผ่านจะขึ้นคำว่า "Success"
- กดเปิด "Use webhook" ถ้าหากยังไม่ได้เปิด
- เชคให้มั่นใจว่าเราตั้งค่าบอทของเราตามไฟล์นี้
- ในไฟล์
member.jsสร้างฟังก์ชันhandleFollowEventไว้ก่อนฟังก์ชันhandleUnknownEventเพื่อเอาไว้จัดการ event ประเภทfollowที่เราจะได้รับตอนมีคนเพิ่มบอตของเราเป็นเพื่อน
function handleFollowEvent(event) {
const userId = event.source.userId;
insertMember(userId);
pushToMember(userId, "ยินดีต้อนรับเข้าสู่ สมาชิก Bot ครับ กรุณาพิมพ์ 'เช็คคะแนน' เพื่อเช็คคะแนนของคุณ");
}คำอธิบาย
ในการที่จะเพิ่มสมาชิกเข้าไปในระบบ เราต้องใช้ userId ที่เราสามารถเอามาได้จาก event.source.userId
const userId = event.source.userId;พอได้ userId มาแล้ว เราก็เพิ่มสมาชิกเข้าไปในระบบด้วยการใช้ฟังก์ชัน insertMember ที่เรามีอยู่แล้ว
insertMember(userId);เสร็จแล้วเราก็ส่งข้อความต้อนรับไปหาสมาชิกด้วยฟังก์ชัน pushToMember ได้เลย
pushToMember(userId, "ยินดีต้อนรับเข้าสู่ สมาชิก Bot ครับ กรุณาพิมพ์ 'เช็คคะแนน' เพื่อเช็คคะแนนของคุณ");- ในฟังก์ชัน
handleLineEventเราจะเรียกใช้ฟังก์ชันhandleFollowEventที่เราพึ่งสร้างไปเมื่อกี้ เมื่อevent.typeคือfollowโดยเราสามารถเพิ่มโค้ดส่วนนี้เข้าไปในswitchstatement ได้เลย
case "follow":
return handleFollowEvent(event);หน้าตาฟังก์ชัน handleLineEvent ตอนนี้ก็จะเป็นแบบนี้
function handleLineEvent(event) {
switch (event.type) {
case "follow":
return handleFollowEvent(event);
default:
return handleUnknownEvent(event);
}
}- ในไฟล์
member.jsสร้างฟังก์ชันhandleUnfollowEventไว้ก่อนฟังก์ชันhandleUnknownEventเพื่อเอาไว้จัดการ event ประเภทunfollowที่เราจะได้รับตอนมีคนบล็อกเราหรือลบเราออกจากเพื่อน
function handleUnfollowEvent(event) {
const userId = event.source.userId;
deleteMember(userId);
}คำอธิบาย
ในการที่จะลบสมาชิกออกจากระบบ เราต้องใช้ userId ที่เราสามารถเอามาได้จาก event.source.userId
const userId = event.source.userId;พอได้ userId มาแล้ว เราก็ลบสมาชิกออกจากระบบด้วยการใช้ฟังก์ชัน deleteMember ที่เรามีอยู่แล้วได้เลย
deleteMember(userId);- ในฟังก์ชัน
handleLineEventเราจะเรียกใช้ฟังก์ชันhandleUnfollowEventที่เราพึ่งสร้างไปเมื่อกี้ เมื่อevent.typeคือunfollowโดยเราสามารถเพิ่มโค้ดส่วนนี้เข้าไปในswitchstatement ได้เลย
case "unfollow":
return handleUnfollowEvent(event);หน้าตาฟังก์ชัน handleLineEvent ตอนนี้ก็จะเป็นแบบนี้
function handleLineEvent(event) {
switch (event.type) {
case "follow":
return handleFollowEvent(event);
case "unfollow":
return handleUnfollowEvent(event);
default:
return handleUnknownEvent(event);
}
}- เนื่องจากฟังก์ชัน
handleFollowEventจะทำงานก็ต่อเมื่อเพิ่มเพื่อน เราจึงต้อง block บอทก่อนเหมือนเป็นการ unfollow - เข้าไปที่ LINE Bot ของเรา เลือก 3 ขีด
- เลือก block
- เลือก unblock เพื่อทำการเพิ่มเพื่อน บอทจะสวัสดีอีกครั้ง
เมื่อสมาชิกส่งคำว่า
เช็คคะแนนให้ส่งข้อความกลับไปหาสมาชิกว่าคุณมีคะแนนสะสม XXX คะแนน
- ในไฟล์
member.jsสร้างฟังก์ชันhandleMessageEventไว้ก่อนฟังก์ชันhandleUnknownEventเพื่อเอาไว้จัดการ event ประเภทmessageที่เราจะได้รับตอนมีคนส่งข้อความมาหาเรา
function handleMessageEvent(event) {
const text = event.message.text;
if (text === "เช็คคะแนน") {
const member = getMemberByUid(event.source.userId);
return replyToMember(event, `คุณมีคะแนนสะสม ${member.points} คะแนน`);
}
}คำอธิบาย
ในการที่เราจะเช็คว่าสมาชิกส่งคำว่า เช็คคะแนน มารึเปล่า เราจะใช้ข้อความ text ที่เราเอามาได้จาก event.message.text
const text = event.message.text;พอได้ข้อความมาแล้ว เราก็เอามาเช็คว่ามันตรงกับคำว่า เช็คคะแนน มั้ย
if (text === "เช็คคะแนน") {
// some other code
}ในการที่เราจะหาสมาชิก เราก็ต้องใช้ userId ที่เราหาได้จาก event.source.userId เหมือนเดิม
const userId = event.source.userId;พอได้ userId แล้ว เราก็หาสมาชิกได้ด้วยการเรียกใช้ฟังก์ชัน getMemberByUid ที่เรามีอยู่แล้ว
const member = getMemberByUid(userId);เสร็จแล้ว เราก็ส่งข้อมูลเกี่ยวกับคะแนนสะสมกลับไปให้สมาชิกได้เลย โดยการใช้ฟังก์ชัน replyToMember
return replyToMember(event, `คุณมีคะแนนสะสม ${member.points} คะแนน`);- ในฟังก์ชัน
handleLineEventเราจะเรียกใช้ฟังก์ชันhandleMessageEventที่เราพึ่งสร้างไปเมื่อกี้ เมื่อevent.typeคือmessageโดยเราสามารถเพิ่มโค้ดส่วนนี้เข้าไปในswitchstatement ได้เลย
case "message":
return handleMessageEvent(event);หน้าตาฟังก์ชัน handleLineEvent ตอนนี้ก็จะเป็นแบบนี้
function handleLineEvent(event) {
switch (event.type) {
case "follow":
return handleFollowEvent(event);
case "unfollow":
return handleUnfollowEvent(event);
case "message":
return handleMessageEvent(event);
default:
return handleUnknownEvent(event);
}
}- ลองส่งข้อความหาน้องบอตดู
สำหรับคนที่ตามไม่ทัน ก็อปโค้ดนี้ไปแทนที่โค้ดในไฟล์ member.js, เปลี่ยน MEMBER_CHANNEL_SECRET และ MEMBER_CHANNEL_ACCESS_TOKEN แล้วกด Save แล้วลองเพิ่มเพื่อน บล็อก และปลดบล็อคดู
โค้ดเต็ม member.js
const express = require("express");
const line = require("@line/bot-sdk");
const { insertMember, deleteMember, getMemberByUid } = require("../persists");
const { replier, pusher } = require("./util");
const memberConfig = {
channelSecret: "MEMBER_CHANNEL_SECRET",
channelAccessToken:
"MEMBER_CHANNEL_ACCESS_TOKEN",
};
const memberWebhookMiddleware = line.middleware(memberConfig);
const memberClient = new line.messagingApi.MessagingApiClient(memberConfig);
const replyToMember = replier(memberClient);
const pushToMember = pusher(memberClient);
const router = express.Router();
router.post("/", memberWebhookMiddleware, handlePostRequest);
/**
* จัดการ request ที่เข้ามาที่ /webhooks/member
* @param {Object} request - คำขอที่เข้ามา
* @param {Object} response - การตอบกลับของคำขอ
* @returns {Promise} - ส่งผลจากการจัดการ event กลับไป
*/
async function handlePostRequest(request, response) {
// หยิบ events ออกมาจาก request body
const { events } = request.body;
// วนเรียกฟังก์ชัน handleLineEvent ที่ event แต่ละตัว
const eventHandledPromises = events.map(handleLineEvent);
// รอให้ event ทั้งหมดถูกจัดการ
const result = await Promise.all(eventHandledPromises);
// ส่งผลจากการจัดการ event กลับไป
return response.send(result);
}
function handleLineEvent(event) {
switch (event.type) {
case "follow":
return handleFollowEvent(event);
case "unfollow":
return handleUnfollowEvent(event);
case "message":
return handleMessageEvent(event);
default:
return handleUnknownEvent(event);
}
}
function handleFollowEvent(event) {
const userId = event.source.userId;
insertMember(userId);
pushToMember(userId, "ยินดีต้อนรับเข้าสู่ สมาชิก Bot ครับ กรุณาพิมพ์ 'เช็คคะแนน' เพื่อเช็คคะแนนของคุณ");
}
function handleUnfollowEvent(event) {
const userId = event.source.userId;
deleteMember(userId);
}
function handleMessageEvent(event) {
const text = event.message.text;
if (text === "เช็คคะแนน") {
const userId = event.source.userId;
const member = getMemberByUid(userId);
return replyToMember(event, `คุณมีคะแนนสะสม ${member.points} คะแนน`);
}
}
function handleUnknownEvent(event) {
return replyToMember(event, "ขอโทษครับ ฉันไม่เข้าใจคำสั่งของคุณ");
}
module.exports = {
router,
pushToMember,
};- สร้างบอตใหม่ตามขั้นตอนในไฟล์นี้
- หลังจากสร้างบอตเสร็จแล้ว ไปที่หน้า "Basic settings" ของบอต
- คัดลอก Channel secret ไปแทนที่คำว่า
MERCHANT_CHANNEL_SECRETในไฟล์merchant.js - ไปที่หน้า "Messaging API" ของบอต
- คัดลอก Channel access token ไปแทนที่คำว่า
MERCHANT_CHANNEL_ACCESS_TOKENในไฟล์เดียวกัน - เลื่อนลงไปที่ "LINE Official Account features" ตรง "Auto-reply messages" ให้กด "Edit" แล้วก็กดปิดฟีเจอร์ทุกอย่างยกเว้น "Webhook"
- ใน Codesandbox, กด Save (
Ctrl+S), โปรแกรมจะเริ่มใหม่โดยอัตโนมัติ - ใน Codesandbox จอครึ่งขวาจะเป็น Browser ที่มีลิงค์อยู่ด้านบน ให้ Copy ลิงค์นั้นไปวางในหน้า "Messaging API" ของบอต ในช่อง "Webhook URL" แล้วต่อท้ายด้วย
/webhooks/merchant
ตัวอย่าง
https://www.your-url.com/webhooks/merchant
- กด "Update" แล้วกด "Verify" เพื่อทดสอบการเชื่อมต่อระหว่าง LINE กับบอต; ถ้าทดสอบผ่านจะขึ้นคำว่า "Success"
- กดเปิด "Use webhook" ถ้าหากยังไม่ได้เปิด แล้วลองส่งอะไรก็ได้ใน LINE ดู
ใน merchant.js เรามีโค้ดอยู่แล้วบ้างเช่น
- ฟังก์ชัน
handleLineEventที่เราเอาไว้จัดการ event จาก Line - ฟังก์ชัน
handleMessageEventที่เราเอาไว้จัดการ event ประเภทmessageซึ่งเราได้ทำส่วนจัดการสมาชิกและการจัดการถ้าข้อความที่ส่งมาไม่ใช่สิ่งที่เรากำหนดไว้
- ใน
merchant.jsสร้างฟังก์ชันhandlePointIncreaseไว้ก่อนฟังก์ชันhandleUnknownEventเอาไว้จัดการการเพิ่มคะแนนสะสมของสมาชิก
function handlePointIncrease(event, text) {
const [_, memberId, pointToAddText] = text.split(" ");
const member = getMemberById(memberId);
if (!member) {
return replyToMerchant(event, `ไม่พบสมาชิกหมายเลข ${memberId}`);
}
const pointToAdd = parseInt(pointToAddText);
if (isNaN(pointToAdd)) {
return replyToMerchant(event, "กรุณาใส่จำนวนคะแนนที่ต้องการเพิ่มเป็นตัวเลขเท่านั้น");
}
updatePointForMember(memberId, member.points + pointToAdd);
pushToMember(member.uid, `คุณได้รับคะแนน ${pointToAdd} คะแนน`);
return replyToMerchant(event, `เพิ่มคะแนน ${pointToAdd} คะแนนให้สมาชิกหมายเลข ${memberId} แล้ว`);
}คำอธิบาย
เนื่องจากรูปแบบคำสั่งเพิ่มคะแนนให้สมาชิก จะมาในรูปแบบ เพิ่มคะแนน <รหัสสมาชิก> <จำนวนแต้มที่ต้องการจะเพิ่ม> เราจะต้องหยิบรหัสสมาชิก และแต้มที่ต้องการจะเพิ่มก่อน โดยการใช้
const [_, memberId, pointToAddText] = text.split(" ");ในการที่เราจะเพิ่มแต้ม เราจะต้องเอาแต้มที่มีอยู่ มาบวกกับ แต้มที่มีอยู่แล้ว แต้มใหม่ = แต้มที่มีอยู่แล้ว + แต้มที่ต้องการจะเพิ่ม
เราก็เลยต้องหาสมาชิกที่เราต้องการจะเพิ่มแต้ม โดยการใช้ฟังก์ชัน getMemberById ที่เรามีอยู่แล้ว
const member = getMemberById(memberId);ถ้าเราหาสมาชิกคนนั้นไม่เจอ เราก็ไม่ควรจะเพิ่มแต้มได้ เราก็จะส่งข้อความไปหาร้านค้าว่า ไม่พบสมาชิกหมายเลข XXXX โดยใช้ฟังก์ชัน replyToMerchant ที่เรามีอยู่แล้วเพื่อตอบกลับไปที่ร้านค้า
if (!member) {
return replyToMerchant(event, `ไม่พบสมาชิกหมายเลข ${memberId}`);
}ตอนนี้คะแนนที่เราได้มาจากคำสั่งของร้านค้าจะยังเป็นข้อความอยู่ เราก็ต้องแปลงมันให้กลายเป็นตัวเลขก่อน โดยการใช้ parseInt แล้วถ้าสิ่งที่เรารับมาไม่ใช่ตัวเลข เราก็ส่งข้อความตอบกลับไปที่ร้านค้าว่า กรุณาใส่จำนวนคะแนนที่ต้องการเพิ่มเป็นตัวเลขเท่านั้น
const pointToAdd = parseInt(pointToAddText);
if (isNaN(pointToAdd)) {
return replyToMerchant(event, "กรุณาใส่จำนวนคะแนนที่ต้องการเพิ่มเป็นตัวเลขเท่านั้น");
}พอเรามีทั้งรหัสสมาชิก และคะแนนใหม่ที่ต้องการจะเปลี่ยนแล้ว เราสามารถใช้ฟังก์ชัน updatePointForMember ในการอัพเดทคะแนนสะสมให้สมาชิกได้
updatePointForMember(memberId, member.points + pointToAdd);หลังจากนั้น เราก็สามารถที่จะส่งข้อความไปแจ้งเตือนสมาชิกได้ โดยการใช้ฟังก์ชัน pushToMember
pushToMember(member.uid, `คุณได้รับคะแนน ${pointToAdd} คะแนน`);แล้วสุดท้าย เราก็ส่งข้อความกลับไปหาร้านค้าว่า เพิ่มคะแนน XXXX คะแนนให้สมาชิกหมายเลข YYY แล้ว
return replyToMerchant(event, `เพิ่มคะแนน ${pointToAdd} คะแนนให้สมาชิกหมายเลข ${memberId} แล้ว`);- ในฟังก์ชัน
handleMessageEventให้เพิ่มเงื่อนไขที่เอาไว้เช็คว่ามีข้อความ เพิ่มคะแนน ส่งมามั้ย แล้วก็เรียกใช้ฟังก์ชันhandlePointIncreaseที่เราสร้างไปเมื่อสักครู่
if (text.includes("เพิ่มคะแนน")) {
return handlePointIncrease(event, text);
}ใน handleMessageEvent ก็จะหน้าตาประมาณ
function handleMessageEvent(event) {
const text = event.message.text;
if (text === "จัดการสมาชิก") {
const members = getAllMembers();
const memberInfoMessages = members.map((member) => `สมาชิกหมายเลข ${member.id} มีคะแนน ${member.points} คะแนน`);
return replyToMerchant(event, ...memberInfoMessages);
}
if (text.includes("เพิ่มคะแนน")) {
return handlePointIncrease(event, text);
}
return replyToMerchant(
event,
"กรุณาพิมพ์ 'จัดการสมาชิก' เพื่อดูข้อมูลสมาชิก เพื่อเพิ่มคะแนนให้สมาชิก",
"หรือพิมพ์ 'เพิ่มคะแนน <รหัสสมาชิก> <คะแนน>' เพื่อเพิ่มคะแนนของสมาชิก",
);
}สำหรับคนที่ตามไม่ทัน ก็อปโค้ดนี้ไปแทนที่โค้ดในไฟล์ merchant.js, เปลี่ยน MERCHANT_CHANNEL_SECRET และ MERCHANT_CHANNEL_ACCESS_TOKEN แล้วกด Save แล้วลองเล่นดู
โค้ดเต็ม merchant.js
const express = require("express");
const line = require("@line/bot-sdk");
const {
getMemberById,
getAllMembers,
updatePointForMember,
getMemberByUid,
} = require("../persists");
const { replier } = require("./util");
const {
pushToMember,
} = require("./member");
const merchantConfig = {
channelSecret: "MERCHANT_CHANNEL_SECRET",
channelAccessToken:
"MERCHANT_ACCESS_TOKEN",
};
const merchantWebhookMiddleware = line.middleware(merchantConfig);
const merchantClient = new line.messagingApi.MessagingApiClient(merchantConfig);
const replyToMerchant = replier(merchantClient);
const router = express.Router();
router.post("/", merchantWebhookMiddleware, handleRequest);
/**
* จัดการ request ที่เข้ามาที่ /webhooks/member
* @param {Object} request - คำขอที่เข้ามา
* @param {Object} response - การตอบกลับของคำขอ
* @returns {Promise} - ส่งผลจากการจัดการ event กลับไป
*/
async function handleRequest(request, response) {
// หยิบ events ออกมาจาก request body
const { events } = request.body;
// วนเรียกฟังก์ชัน handleLineEvent ที่ event แต่ละตัว
const eventHandledPromises = events.map(handleLineEvent);
// รอให้ event ทั้งหมดถูกจัดการ
const result = await Promise.all(eventHandledPromises);
// ส่งผลจากการจัดการ event กลับไป
return response.send(result);
}
function handleLineEvent(event) {
switch (event.type) {
case "message":
return handleMessageEvent(event);
default:
return handleUnknownEvent(event);
}
}
function handleMessageEvent(event) {
const text = event.message.text;
if (text === "จัดการสมาชิก") {
const members = getAllMembers();
const memberInfoMessages = members.map((member) => `สมาชิกหมายเลข ${member.id} มีคะแนน ${member.points} คะแนน`);
return replyToMerchant(event, ...memberInfoMessages);
}
if (text.includes("เพิ่มคะแนน")) {
return handlePointIncrease(event, text);
}
return replyToMerchant(
event,
"กรุณาพิมพ์ 'จัดการสมาชิก' เพื่อดูข้อมูลสมาชิก เพื่อเพิ่มคะแนนให้สมาชิก",
"หรือพิมพ์ 'เพิ่มคะแนน <รหัสสมาชิก> <คะแนน>' เพื่อเพิ่มคะแนนของสมาชิก",
);
}
function handlePointIncrease(event, text) {
const [_, memberId, pointToAddText] = text.split(" ");
const member = getMemberById(memberId);
if (!member) {
return replyToMerchant(event, `ไม่พบสมาชิกหมายเลข ${memberId}`);
}
const pointToAdd = parseInt(pointToAddText);
if (isNaN(pointToAdd)) {
return replyToMerchant(event, "กรุณาใส่จำนวนคะแนนที่ต้องการเพิ่มเป็นตัวเลขเท่านั้น");
}
updatePointForMember(memberId, member.points + pointToAdd);
pushToMember(member.uid, `คุณได้รับคะแนน ${pointToAdd} คะแนน`);
return replyToMerchant(event, `เพิ่มคะแนน ${pointToAdd} คะแนนให้สมาชิกหมายเลข ${memberId} แล้ว`);
}
/**
* จัดการ event ที่ไม่รู้จัก
* @param {Object} event - event ที่จะถูกจัดการ
*/
function handleUnknownEvent(event) {
return replyToMerchant(event, "ขอโทษครับ ฉันไม่เข้าใจคำสั่งของคุณ");
}
module.exports = router;