โปรดติดตามเราบน YouTube เพื่อเขาถึงเนื้อหาการที่เป็นประโยชน์ 🎉
กดที่นี่เพื่อเข้าลิงก์ Quick reply Template บน Google Sheets
*** หลังจากเปิดลิงก์แล้วให้ทำการทำสำเนาไฟล์ไปไว้ใน ไดรฟ์ ของคุณเอง เพื่อให้สามารถแก้ไขข้อมูลได้
จากนั้นให้ทำการค้นหา Sheet ID จากลิงก์ของ Google sheets ไฟล์ที่ทำสำเนาไว้
เช่น https://docs.google.com/spreadsheets/d/1N_AXO_wEtU690Ybnz4rc6JTNFknAvD9Gs-XVhkk7yoo/edit?gid=1381850363#gid=1381850363
จากตัวอย่างของ ลิงก์ ข้อมูล Sheet ID ของคุณคือส่วนที่เป็นตัวหนังสือสีแดง ซึ่ง คือ
1N_AXO_wEtU690Ybnz4rc6JTNFknAvD9Gs-XVhkk7yoo
ในส่วนนี้เราต้องนำ Sheet ID ไปใช้งานเป็นพารามิเตอร์ตัวที่สอง เพื่อเรียก ฟังก์ชั่นหลัก
สคริปต์ได้มีการทดสอบทุกแอคชั่น และ แก้ไข บั๊กที่เกิดใน YouTube Video EP2 เรียบร้อยแล้ว
ให้ทำการ
const quickReplySheetID='ใส่ Sheet ID ของคุณลงไป';
const quickReplyId = 'ใส่ Quick Reply ID ที่ต้องการใช้ โดยจะต้องตรงกับ ข้อมูลในคอลัมน์ quickreplyId ของ GoogleSheet ID ที่ระบุด้านบน';
const finalMessageWithQuickReply = createMessageWithQuickReply(Message Object เดิมของคุณที่พร้อมส่ง, quickReplySheetID, quickReplyId)
หมายเหตุ ในการใช้งานที่ซับซ้อนเพิ่มขึ้น ข้อมูล quickreplyID คุณสามารถนำไปผูกกับข้อมูล รายการข้อความคำตอบ บน Google Sheet เดิมของคุณได้ เพื่อให้สามารถใช้ Quick Reply แบบแปรผันตาม ข้อความคำตอบ บน Google Sheet เดิมของคุณ แล้วนำมาแมพค่าให้ quickreplyID ก็ได้เช่นกัน
👉 แล้วอย่าลืมเปลี่ยนตัวแปรข้อความที่ใช้ในการส่งจาก Message Object เดิมของคุณที่พร้อมส่ง ไปเป็น finalMessageWithQuickReply
โดยสคริปต์เวอร์ชั่นใหม่ที่ผ่านการอัพเกรดด้วย ฟังก์ชั่นเพิ่ม Quick Reply จากเรา เพียงเท่านี้ การส่งข้อความแบบเดิมของคุณก็จะมี Quick Reply ให้เพื่อนๆใน LINE OA ของคุณสามารถกดเพื่อใช้งานได้แล้ว
/**
* Creates LINE message(s) with quick reply options from Google Sheet data
* Adds quickReply to the last message item if messageObject is an array
* Ensures the quickReply property is the last property in the message object
*
* @param {Object|Array} messageObject - Single message object or array of message objects
* @param {string} quickReplyId - The ID of the quick reply to retrieve from the Google Sheet
* @param {string} quickReplySheetID - Optional spreadsheet ID for the quick reply data
* @return {Object|Array} Properly formatted LINE message(s) with quick reply
*/
function createMessageWithQuickReply(messageObject, quickReplySheetID, quickReplyId) {
// Check if messageObject is null or undefined
if (!messageObject) {
throw new Error('messageObject must be provided');
}
// Get quick reply data from the Google Sheet
const quickReplyObject = quickReplyId ? getQuickReplyFromSheet(quickReplyId, quickReplySheetID) : null;
// Logger.log("quickReplyObject :" + quickReplyObject)
// Handle messages object with messages array
if (messageObject.messages && Array.isArray(messageObject.messages)) {
// Create a deep copy to avoid modifying the original
const messagesObj = JSON.parse(JSON.stringify(messageObject));
// Validate that the array is not empty
if (messagesObj.messages.length === 0) {
throw new Error('messageObject.messages array cannot be empty');
}
// Only add quickReply to the last message in the array
const lastIndex = messagesObj.messages.length - 1;
// Validate the last message has a type
if (!messagesObj.messages[lastIndex] || !messagesObj.messages[lastIndex].type) {
throw new Error('Last message in the array must have a type property');
}
// Remove any existing quickReply from all messages
messagesObj.messages.forEach(msg => {
if (msg.quickReply) {
delete msg.quickReply;
}
});
// If quickReplyObject is provided, add it to the last message
if (quickReplyObject && quickReplyObject.items && quickReplyObject.items.length > 0) {
// Add quick reply to the last message
// Ensure it's the last property by recreating the object with desired property order
const lastMessage = messagesObj.messages[lastIndex];
const newLastMessage = {};
// Copy all properties except quickReply
Object.keys(lastMessage).forEach(key => {
newLastMessage[key] = lastMessage[key];
});
// Add quickReply as the last property
newLastMessage.quickReply = quickReplyObject;
// Replace the last message with the new one
// Logger.log("Replace the last message with the new one")
messagesObj.messages[lastIndex] = newLastMessage;
}
return messagesObj;
}
// Handle array of messages
else if (Array.isArray(messageObject)) {
// Validate that the array is not empty
if (messageObject.length === 0) {
throw new Error('messageObject array cannot be empty');
}
// Create a deep copy of the array to avoid modifying the original
const messages = JSON.parse(JSON.stringify(messageObject));
// Only add quickReply to the last message in the array
const lastIndex = messages.length - 1;
// Validate the last message has a type
if (!messages[lastIndex] || !messages[lastIndex].type) {
throw new Error('Last message in the array must have a type property');
}
// Remove any existing quickReply from all messages
messages.forEach(msg => {
if (msg.quickReply) {
delete msg.quickReply;
}
});
// If quickReplyObject is provided, add it to the last message
if (quickReplyObject && quickReplyObject.items && quickReplyObject.items.length > 0) {
// Add quick reply to the last message
// Ensure it's the last property by recreating the object with desired property order
const lastMessage = messages[lastIndex];
const newLastMessage = {};
// Copy all properties except quickReply
Object.keys(lastMessage).forEach(key => {
newLastMessage[key] = lastMessage[key];
});
// Add quickReply as the last property
newLastMessage.quickReply = quickReplyObject;
// Replace the last message with the new one
messages[lastIndex] = newLastMessage;
}
return messages;
}
// Handle single message object
else if (typeof messageObject === 'object') {
// Validate the message has a type
if (!messageObject.type) {
throw new Error('messageObject must have a type property');
}
// Create a new object to ensure quickReply is the last property
const message = {};
// Copy all properties except quickReply
Object.keys(messageObject).forEach(key => {
if (key !== 'quickReply') {
message[key] = messageObject[key];
}
});
// If quickReplyObject is provided, add it to the message
if (quickReplyObject && quickReplyObject.items && quickReplyObject.items.length > 0) {
// Add quick reply as the last property
message.quickReply = quickReplyObject;
}
return message;
}
else {
throw new Error('messageObject must be an object or an array of objects');
}
}
/**
* Gets quick reply data from Google Sheets based on the quick reply ID
*
* @param {string} quickReplyId - The ID of the quick reply set to retrieve
* @param {string} quickReplySheetID - Optional spreadsheet ID for the quick reply data
* @return {Object} A properly formatted quick reply object
*/
function getQuickReplyFromSheet(quickReplyId, quickReplySheetID) {
try {
// Use the active spreadsheet if no ID is provided
let ss;
if (quickReplySheetID) {
try {
ss = SpreadsheetApp.openById(quickReplySheetID);
} catch (e) {
console.log(`Error opening spreadsheet by ID: ${e.message}`);
ss = SpreadsheetApp.getActiveSpreadsheet();
}
} else {
ss = SpreadsheetApp.getActiveSpreadsheet();
}
if (!ss) {
throw new Error('Could not access spreadsheet');
}
// First, check if the quick reply ID exists in the master sheet
const masterSheet = ss.getSheetByName('quickreplyData');
if (!masterSheet) {
throw new Error('quickreplyData sheet not found');
}
const masterData = masterSheet.getDataRange().getValues();
const masterHeaders = masterData[0];
// Logger.log("Master headers: " + masterHeaders.join(", "));
const idColIndex = masterHeaders.indexOf('quickreplyId');
const nameColIndex = masterHeaders.indexOf('name');
const statusColIndex = masterHeaders.indexOf('status');
if (idColIndex === -1) {
throw new Error('quickreplyId column not found in quickreplyData sheet');
}
// Find the quick reply in the master sheet
let quickReplyRow = -1;
let isActive = true;
for (let i = 1; i < masterData.length; i++) {
// Logger.log(`Checking row ${i}: ${masterData[i][idColIndex]} vs ${quickReplyId}`);
if (masterData[i][idColIndex] === quickReplyId) {
// Logger.log("Found matching quickreplyId");
quickReplyRow = i;
// Check if the quick reply is active
if (statusColIndex !== -1) {
isActive = masterData[i][statusColIndex] === 'active';
}
break;
}
}
if (quickReplyRow === -1) {
throw new Error(`Quick reply with ID ${quickReplyId} not found in quickreplyData sheet`);
}
if (!isActive) {
Logger.log(`Warning: Quick reply with ID ${quickReplyId} is not active`);
return { items: [] }; // Return empty items if inactive
}
// Initialize items array
const items = [];
// Process each sheet
// Process action columns (action1, action2, etc.)
for (let i = 1; i <= 13; i++) {
const actionColIndex = masterHeaders.indexOf(`action${i}`);
if (actionColIndex !== -1 && masterData[quickReplyRow][actionColIndex]) {
const actionId = masterData[quickReplyRow][actionColIndex];
if (actionId) {
// Logger.log(`Found action${i}: ${actionId}`);
const actionItem = getActionById(ss, actionId);
if (actionItem) {
items.push(actionItem);
}
}
}
}
// Validate the number of items
if (items.length > 13) {
Logger.log(`Warning: Quick reply ${quickReplyId} has ${items.length} items, which exceeds the maximum of 13. Only the first 13 will be used.`);
items.splice(13); // Keep only the first 13 items
}
// Logger.log(`Final items count: ${items.length}`);
// Return the quick reply object
return items.length > 0 ? { items: items } : null;
} catch (error) {
console.log(`Error getting quick reply from sheet: ${error.message}`);
return null;
}
}
/**
* Gets an action item by its ID from the appropriate sheet
*
* @param {SpreadsheetApp.Spreadsheet} quickReplySheetID - The spreadsheet
* @param {string} actionId - The action ID
* @return {Object} A properly formatted quick reply item
*/
function getActionById(quickReplySheetID, actionId) {
try {
// Determine which sheet to look in based on the action ID prefix
let sheetName;
if (actionId.startsWith('msg-')) {
sheetName = 'message';
} else if (actionId.startsWith('pb-')) {
sheetName = 'postback';
} else if (actionId.startsWith('uri-')) {
sheetName = 'uri';
} else if (actionId.startsWith('dt-')) {
sheetName = 'datetimepicker';
} else if (actionId.startsWith('cam-')) {
sheetName = 'camera';
} else if (actionId.startsWith('cr-')) {
sheetName = 'cameraroll';
} else if (actionId.startsWith('loc-')) {
sheetName = 'location';
} else if (actionId.startsWith('cb-')) {
sheetName = 'clipboard';
} else {
Logger.log(`Unknown action ID prefix: ${actionId}`);
return null;
}
const sheet = quickReplySheetID.getSheetByName(sheetName);
if (!sheet) {
Logger.log(`Sheet ${sheetName} not found`);
return null;
}
const data = sheet.getDataRange().getValues();
if (data.length <= 1) {
Logger.log(`Sheet ${sheetName} has no data rows`);
return null;
}
const headers = data[0];
const idColIndex = headers.indexOf('ItemId');
if (idColIndex === -1) {
Logger.log(`actionId ${actionId} column not found in ${sheetName} sheet`);
return null;
}
// Find the action in the sheet
let actionRow = -1;
for (let i = 1; i < data.length; i++) {
if (data[i][idColIndex] === actionId) {
actionRow = i;
break;
}
}
if (actionRow === -1) {
Logger.log(`Action with ID ${actionId} not found in ${sheetName} sheet`);
return null;
}
// Create the action item based on the sheet type
return createActionItem(sheetName, data[actionRow], headers);
} catch (error) {
Logger.log(`Error getting action by ID: ${error.message}`);
return null;
}
}
/**
* Creates a quick reply action item based on the sheet type and row data
*
* @param {string} sheetName - The name of the sheet
* @param {Array} row - The row data
* @param {Array} headers - The header row
* @return {Object} A properly formatted quick reply item
*/
function createActionItem(sheetName, row, headers) {
try {
// Basic item structure
const item = {
type: 'action',
action: {}
};
// Helper function to trim label to 20 characters
function trimLabel(label) {
if (label && label.length > 20) {
Logger.log(`Trimming label from ${label.length} to 20 characters: ${label}`);
return label.substring(0, 20);
}
return label;
}
// Helper function to format date values based on mode
function formatDateForMode(dateValue, mode) {
if (!dateValue) return null;
//Logger.log(`Formatting date value: ${dateValue} for mode: ${mode}`);
try {
// Parse the date value
let date;
if (typeof dateValue === 'string') {
// Try to parse the date string
const parts = dateValue.split('-');
if (parts.length === 3) {
// Format: YYYY-MM-DD
const year = parseInt(parts[0], 10);
const month = parseInt(parts[1], 10) - 1; // Months are 0-indexed in JS
const day = parseInt(parts[2], 10);
date = new Date(year, month, day);
} else {
date = new Date(dateValue);
}
} else if (dateValue instanceof Date) {
date = dateValue;
} else {
date = new Date(dateValue);
}
if (isNaN(date.getTime())) {
Logger.log(`Invalid date value: ${dateValue}`);
return null;
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
let formattedDate;
if (mode === 'date') {
formattedDate = `${year}-${month}-${day}`;
} else if (mode === 'time') {
formattedDate = `${hours}:${minutes}`;
} else if (mode === 'datetime') {
formattedDate = `${year}-${month}-${day}T${hours}:${minutes}`;
} else {
Logger.log(`Unknown mode: ${mode}`);
return null;
}
//Logger.log(`Formatted date: ${formattedDate}`);
return formattedDate;
} catch (e) {
Logger.log(`Error formatting date: ${e.message}`);
return null;
}
}
// Add common properties based on the sheet type
switch (sheetName) {
case 'message':
// Find column indexes
const labelCol = headers.indexOf('Label');
const textCol = headers.indexOf('Text');
const imageUrlCol = headers.indexOf('ImageURL');
if (labelCol === -1 || textCol === -1 || row[labelCol] === '' || row[textCol] === '') {
Logger.log(`Missing required fields in message sheet`);
return null;
}
item.action = {
type: 'message',
label: trimLabel(row[labelCol]),
text: row[textCol]
};
// Add imageUrl if available
if (imageUrlCol !== -1 && row[imageUrlCol]) {
item.imageUrl = row[imageUrlCol];
}
break;
case 'postback':
// Find column indexes
const pbLabelCol = headers.indexOf('Label');
const dataCol = headers.indexOf('Data');
const displayTextCol = headers.indexOf('DisplayText');
const pbImageUrlCol = headers.indexOf('ImageURL');
const inputOptionCol = headers.indexOf('InputOption');
const fillInTextCol = headers.indexOf('FillInText');
if (pbLabelCol === -1 || dataCol === -1 || row[pbLabelCol] === '' || row[dataCol] === '') {
Logger.log(`Missing required fields in postback sheet`);
return null;
}
item.action = {
type: 'postback',
label: trimLabel(row[pbLabelCol]),
data: row[dataCol]
};
// Add optional fields if available
if (displayTextCol !== -1 && row[displayTextCol]) {
item.action.displayText = row[displayTextCol];
}
if (pbImageUrlCol !== -1 && row[pbImageUrlCol]) {
item.imageUrl = row[pbImageUrlCol];
}
if (inputOptionCol !== -1 && row[inputOptionCol]) {
item.action.inputOption = row[inputOptionCol];
}
if (fillInTextCol !== -1 && row[fillInTextCol]) {
item.action.fillInText = row[fillInTextCol];
}
break;
case 'uri':
// Find column indexes
const uriLabelCol = headers.indexOf('Label');
const uriCol = headers.indexOf('URI');
const uriImageUrlCol = headers.indexOf('ImageURL');
if (uriLabelCol === -1 || uriCol === -1 || row[uriLabelCol] === '' || row[uriCol] === '') {
Logger.log(`Missing required fields in uri sheet`);
return null;
}
item.action = {
type: 'uri',
label: trimLabel(row[uriLabelCol]),
uri: row[uriCol]
};
// Add imageUrl if available
if (uriImageUrlCol !== -1 && row[uriImageUrlCol]) {
item.imageUrl = row[uriImageUrlCol];
}
break;
case 'datetimepicker':
// Find column indexes
const dtLabelCol = headers.indexOf('Label');
const dtDataCol = headers.indexOf('Data');
const modeCol = headers.indexOf('Mode');
const initialCol = headers.indexOf('Initial');
const maxCol = headers.indexOf('Max');
const minCol = headers.indexOf('Min');
const dtImageUrlCol = headers.indexOf('ImageURL');
if (dtLabelCol === -1 || dtDataCol === -1 || modeCol === -1 ||
row[dtLabelCol] === '' || row[dtDataCol] === '' || row[modeCol] === '') {
Logger.log(`Missing required fields in datetimepicker sheet`);
return null;
}
const mode = row[modeCol].trim().toLowerCase();
// Logger.log(`Datetimepicker mode: ${mode}`);
// Validate mode
if (mode !== 'date' && mode !== 'time' && mode !== 'datetime') {
Logger.log(`Invalid datetimepicker mode: ${mode}. Must be 'date', 'time', or 'datetime'.`);
return null;
}
item.action = {
type: 'datetimepicker',
label: trimLabel(row[dtLabelCol]),
data: row[dtDataCol],
mode: row[modeCol]
};
// Add optional fields if available, with proper formatting
if (initialCol !== -1 && row[initialCol]) {
const formattedInitial = formatDateForMode(row[initialCol], mode);
if (formattedInitial) {
item.action.initial = formattedInitial;
}
}
if (maxCol !== -1 && row[maxCol]) {
const formattedMax = formatDateForMode(row[maxCol], mode);
if (formattedMax) {
item.action.max = formattedMax;
}
}
if (minCol !== -1 && row[minCol]) {
const formattedMin = formatDateForMode(row[minCol], mode);
if (formattedMin) {
item.action.min = formattedMin;
}
}
// Log the formatted datetimepicker action for debugging
//Logger.log(`Datetimepicker action: ${JSON.stringify(item.action)}`);
if (dtImageUrlCol !== -1 && row[dtImageUrlCol]) {
item.imageUrl = row[dtImageUrlCol];
}
break;
case 'camera':
// Find column indexes
const camLabelCol = headers.indexOf('Label');
const camImageUrlCol = headers.indexOf('ImageURL');
if (camLabelCol === -1 || row[camLabelCol] === '') {
Logger.log(`Missing required fields in camera sheet`);
return null;
}
item.action = {
type: 'camera',
label: trimLabel(row[camLabelCol])
};
// Add imageUrl if available
if (camImageUrlCol !== -1 && row[camImageUrlCol]) {
item.imageUrl = row[camImageUrlCol];
}
break;
case 'cameraroll':
// Find column indexes
const crLabelCol = headers.indexOf('Label');
const crImageUrlCol = headers.indexOf('ImageURL');
if (crLabelCol === -1 || row[crLabelCol] === '') {
Logger.log(`Missing required fields in cameraroll sheet`);
return null;
}
item.action = {
type: 'cameraRoll',
label: trimLabel(row[crLabelCol])
};
// Add imageUrl if available
if (crImageUrlCol !== -1 && row[crImageUrlCol]) {
item.imageUrl = row[crImageUrlCol];
}
break;
case 'location':
// Find column indexes
const locLabelCol = headers.indexOf('Label');
const locImageUrlCol = headers.indexOf('ImageURL');
if (locLabelCol === -1 || row[locLabelCol] === '') {
Logger.log(`Missing required fields in location sheet`);
return null;
}
item.action = {
type: 'location',
label: trimLabel(row[locLabelCol])
};
// Add imageUrl if available
if (locImageUrlCol !== -1 && row[locImageUrlCol]) {
item.imageUrl = row[locImageUrlCol];
}
break;
case 'clipboard':
// Find column indexes
const cbLabelCol = headers.indexOf('Label');
const clipboardTextCol = headers.indexOf('ClipboardText');
const cbImageUrlCol = headers.indexOf('ImageURL');
if (cbLabelCol === -1 || clipboardTextCol === -1 ||
row[cbLabelCol] === '' || row[clipboardTextCol] === '') {
Logger.log(`Missing required fields in clipboard sheet`);
return null;
}
item.action = {
type: 'clipboard',
label: trimLabel(row[cbLabelCol]),
clipboardText: row[clipboardTextCol]
};
// Add imageUrl if available
if (cbImageUrlCol !== -1 && row[cbImageUrlCol]) {
item.imageUrl = row[cbImageUrlCol];
}
break;
default:
Logger.log(`Unknown sheet type: ${sheetName}`);
return null;
}
return item;
} catch (error) {
Logger.log(`Error creating action item: ${error.message}`);
return null;
}
}