├── LICENSE ├── readme.md └── app.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Plueone-LINE bot - Jun Shawn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # LINE 加一紀錄機器人 ( PlusOne Bot ) 2 | > _幫你紀錄誰在 LINE 群組傳加一_ 3 | 4 | LINE 群組開團購、報名課程時,大家瘋傳加一,手動紀錄有夠累~用「LINE 加一紀錄機器人」自動紀錄傳「+1」的使用者到 Google 試算表,並回傳給你! 5 | 我把完整的設定教學放在個人網站,[點我看加一 LIND BOT 自訂教學](https://jcshawn.com/addone-linebot/) 6 | 7 | 2022 3/30 Update : 改善程式碼執行效能,並將重複程式碼模組化方便維護。 8 | ## 動機 Motivation 9 | 10 | 這是我為我媽的瑜珈老師設計的一支 LINE 機器人,那位老師以前都是手動紀錄群組喊「+1」預約上課的同學 11 | 因此這隻機器人是以「課程預約」為出發點設計的,你也可以修改成其他的功能。 12 | 13 | 14 | ## 功能 Features 15 | 16 | - 自動識別傳「+1」的使用者,並將 LINE 名稱記錄到名單 17 | - 搭配 Google 試算表做免費資料庫,快速好用 18 | - 支援「+2」( 預約兩位 ) / 「-1」( 取消預約 ) 功能 19 | - 支援候補名額與自動替補 20 | - 使用 Google App Script 語法開發 21 | 22 | ## 實機測試 Demo 23 | 這是課程的群組截圖,群組只要有人傳 +1,機器人會自動記錄,並回傳告知報名成功與剩下多少名額: 24 | 25 | 26 | 27 | 傳指定關鍵字「名單」就能讓機器人傳送完整的報名名單: 28 | 29 | 30 | 31 | 資料都是暫存在 Google 試算表裡,不用另建伺服器或資料庫: 32 | 33 | 34 | 35 | ## 使用方法 How to Use 36 | 37 | 1. 將 app.js 的內容複製,貼到你的 Google App Script 專案上 38 | 39 | 40 | 2. 在 CHANNEL_ACCESS_TOKEN 的引號裡填入你的 LINE API Token 權杖: 41 | ```sh 42 | var CHANNEL_ACCESS_TOKEN = "***"; 43 | ``` 44 | 45 | 在第 18 行的 sheet_url 的引號裡填入你的 Google 試算表連結: 46 | 47 | ```sh 48 | var sheet_url = 'https://docs.google.com/spreadsheets/...' 49 | ``` 50 | 51 | 3. 點選 App Script 網頁的部署按鈕,選擇「新增」: 52 | 53 | 54 | 4. 種類設定為「網路應用程式」: 55 | 56 | 57 | 5. 將存取權限改為「所有人」,再按部署: 58 | 59 | 60 | 6. 接著瀏覽器會出現小視窗,點按「授與存取權」: 61 | 62 | 63 | 7. 選取 Google 帳號後,點選左下小灰字「顯示進階設定」,並點選做下方的「前往 ***」( 此為正常流程 ): 64 | 65 | 66 | 67 | 8. 點選允許: 68 | 69 | 70 | 71 | 9. 將下面的網址複製起來,貼到你的 LINE Bot Console 的 Webhook: 72 | 73 | 74 | 75 | ## 客製化 Customization 76 | 77 | 78 | 除了 LINE Token 跟 Google Sheet 連結之外,你也可以自訂程式的一些細項或變數名稱,我將一些重要的變數列在下面表格: 79 | 80 | 變數名稱 | 用途 | 備註 81 | --------------|---------|------------------------ 82 | userMessage | 使用者傳送的文字訊息內容 | string format 83 | user_id | 使用者的 ID 字串 | 搭配第五十行的 User Info API,查詢使用者名稱 84 | sheet_name | Google Sheet 的工作表名稱 | 請填入正確名稱。否則會抓不到 85 | reserve_list | 工作表的全部資料 | 可以自訂修改,但要用 ctrl + F 全部修改 86 | current_list_row | 資料表的最大行數( 最後一筆資料的行數 ) | .getLastRow() 語法 87 | reply_message | 要回傳給使用者的訊息內容 | JSON Format,**請勿直接填入訊息文字**,請參考 LINE 官方的 API 文件 88 | current_hour | 判斷使用者呼叫機器人的時間( 取小時 )| "HH" 是小時格式,請爬文「App Script get current time 」 89 | 90 | ### reply_message 回覆訊息自訂 91 | reply_message 必須是一個 JSON 格式的內容,以文字訊息為例,格式如下: 92 | 93 | ```sh 94 | reply_message = [{ 95 | "type":"text", // 除非是最後一句,每一句後面要加逗號 96 | "text":"引號內打要回傳的文字" 97 | }] 98 | ``` 99 | 100 | 圖片、貼圖、選單、和 Flex Message 圖文格式也是可以用的,詳情請到 LINE 官方 API 文件查看。 101 | 102 | ## 參考資料 103 | - [用 Line Bot 來搜尋 Google 試算表的資料 - (02)Line Bot 設定 | Boris 的分享小站](https://www.youtube.com/watch?v=Bjg_vZnDHbc) 104 | - [LINE 官方 Messaging API 開發文件](https://developers.line.biz/zh-hant/docs/messaging-api/) 105 | - [Google App Script 開發文件](https://developers.google.com/apps-script/reference/document) 106 | - [How to get the current time in Google spreadsheet using script editor?](https://stackoverflow.com/questions/10182020/how-to-get-the-current-time-in-google-spreadsheet-using-script-editor) 107 | ## License 108 | 109 | MIT License 110 | 歡迎自行運用此份專案於商業與個人用途,如果你願意標記我為出處的話,將對我是莫大的鼓勵,感謝! 111 | Feel free to fork this project and use it for your own work. However, it would be great if you credit me. 112 | 113 | 114 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 作者 : Chang Chun Shawn ( jcshawn.com ) 3 | * 程式名稱 : 加一 LINE 紀錄機器人 4 | * 簡述 : 這是一個可以紀錄聊天室或群組傳「+1」訊息使用者的 LINE 機器人,將資料存放在 Google Sheet 中,基於 App Script 語法 5 | * 授權: Apache 2.0 6 | * 聯絡方式: contact@jcshawn.com 7 | * 最新更新 : 2022 / 3 / 30 8 | */ 9 | 10 | function doPost(e) { 11 | // LINE Messenging API Token 12 | var CHANNEL_ACCESS_TOKEN = ''; // LINE Bot API Token 13 | // 以 JSON 格式解析 User 端傳來的 e 資料 14 | var msg = JSON.parse(e.postData.contents); 15 | 16 | // for debugging 17 | Logger.log(msg); 18 | console.log(msg); 19 | 20 | /* 21 | * LINE API JSON 解析資訊 22 | * 23 | * replyToken : 一次性回覆 token 24 | * user_id : 使用者 user id,查詢 username 用 25 | * userMessage : 使用者訊息,用於判斷是否為預約關鍵字 26 | * event_type : 訊息事件類型 27 | */ 28 | const replyToken = msg.events[0].replyToken; 29 | const user_id = msg.events[0].source.userId; 30 | const userMessage = msg.events[0].message.text; 31 | const event_type = msg.events[0].source.type; 32 | 33 | /* 34 | * Google Sheet 資料表資訊設定 35 | * 36 | * 將 sheet_url 改成你的 Google sheet 網址 37 | * 將 sheet_name 改成你的工作表名稱 38 | */ 39 | const sheet_url = 'https://docs.google.com/spreadsheets/d/******'; 40 | const sheet_name = 'reserve'; 41 | const SpreadSheet = SpreadsheetApp.openByUrl(sheet_url); 42 | const reserve_list = SpreadSheet.getSheetByName(sheet_name); 43 | /* 44 | * 預約人數設定 45 | * 46 | * maxium_member : 正式預約人數上限 47 | * waiting_start : 候補人數開始的欄位,無需修改 48 | * waiting_member : 開放候補人數 49 | */ 50 | const maxium_member = 40; 51 | const waiting_start = maxium_member+1; 52 | const waiting_member = 3; 53 | 54 | // 必要參數宣告 55 | var current_hour = Utilities.formatDate(new Date(), "Asia/Taipei", "HH"); // 取得執行時的當下時間 56 | var current_list_row = reserve_list.getLastRow(); // 取得工作表最後一欄( 直欄數 ) 57 | var reply_message = []; // 空白回覆訊息陣列,後期會加入 JSON 58 | 59 | // 查詢傳訊者的 LINE 帳號名稱 60 | function get_user_name() { 61 | // 判斷為群組成員還是單一使用者 62 | switch (event_type) { 63 | case "user": 64 | var nameurl = "https://api.line.me/v2/bot/profile/" + user_id; 65 | break; 66 | case "group": 67 | var groupid = msg.events[0].source.groupId; 68 | var nameurl = "https://api.line.me/v2/bot/group/" + groupid + "/member/" + user_id; 69 | break; 70 | } 71 | 72 | try { 73 | // 呼叫 LINE User Info API,以 user ID 取得該帳號的使用者名稱 74 | var response = UrlFetchApp.fetch(nameurl, { 75 | "method": "GET", 76 | "headers": { 77 | "Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN, 78 | "Content-Type": "application/json" 79 | }, 80 | }); 81 | var namedata = JSON.parse(response); 82 | var reserve_name = namedata.displayName; 83 | } 84 | catch { 85 | reserve_name = "not avaliable"; 86 | } 87 | return String(reserve_name) 88 | } 89 | 90 | // 回傳訊息給line 並傳送給使用者 91 | function send_to_line() { 92 | var url = 'https://api.line.me/v2/bot/message/reply'; 93 | UrlFetchApp.fetch(url, { 94 | 'headers': { 95 | 'Content-Type': 'application/json; charset=UTF-8', 96 | 'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN, 97 | }, 98 | 'method': 'post', 99 | 'payload': JSON.stringify({ 100 | 'replyToken': replyToken, 101 | 'messages': reply_message, 102 | }), 103 | }); 104 | } 105 | 106 | // 將輸入值 word 轉為 LINE 文字訊息格式之 JSON 107 | function format_text_message(word) { 108 | let text_json = [{ 109 | "type": "text", 110 | "text": word 111 | }] 112 | 113 | return text_json; 114 | } 115 | 116 | 117 | var reserve_name = get_user_name(); 118 | 119 | if (typeof replyToken === 'undefined') { 120 | return; 121 | }; 122 | 123 | if (userMessage == "+1" | userMessage == "加一" | userMessage == "+1" | userMessage == "十1") { 124 | // 檢查是否在晚上七點之前傳送 125 | if (current_hour >= 0 & current_hour <= 19 | current_hour >= 21) { 126 | if (current_list_row < maxium_member) { 127 | reserve_list.getRange(current_list_row + 1, 1).setValue(reserve_name); 128 | current_list_row = reserve_list.getLastRow(); 129 | 130 | reply_message = format_text_message(reserve_name + "成功預約 🙆,是第 " + current_list_row + " 位。" + "還有 " + (maxium_member - current_list_row) + " 位名額") 131 | 132 | } 133 | // 人數超過最大正式名額時,進入候補 134 | else if (current_list_row >= maxium_member & current_list_row < (waiting_member + maxium_member)) { 135 | reserve_name = "候補:" + reserve_name; 136 | reserve_list.getRange(current_list_row + 1, 1).setValue(reserve_name); 137 | reply_message = format_text_message("超過 40 人。" + reserve_name + " 為候補預約"); 138 | 139 | } 140 | else { 141 | reply_message = format_text_message("⚠️ 報名額滿!已達 " + maxium_member + "人"); 142 | } 143 | } 144 | else { 145 | reply_message = format_text_message("現在不是報名時間喔 ~ ,請在 00:00 - 19:00 預約"); 146 | } 147 | 148 | send_to_line() 149 | } 150 | 151 | else if (userMessage == "+2" | userMessage == "加二" | userMessage == "十2") { 152 | if (current_hour >= 0 & current_hour <= 19) { 153 | if (current_list_row < maxium_member) { 154 | let name_array = [[reserve_name], [reserve_name]]; 155 | reserve_list.getRange(current_list_row + 1, 1, 2, 1).setValues(name_array); 156 | current_list_row = current_list_row + 2; 157 | 158 | reply_message = format_text_message(reserve_name + "成功預約兩位 🙆" + "還有" + (maxium_member - current_list_row) + "位名額"); 159 | 160 | } 161 | 162 | else if (current_list_row >= maxium_member & current_list_row < maxium_member + 2) { // +2 時不給候補 163 | let waiting_list_name = "候補:" + reserve_name; 164 | let waiting_names_array = [[waiting_list_name], [waiting_list_name]]; 165 | reserve_list.getRange(current_list_row + 1, 1, 2, 1).setValues(waiting_names_array); 166 | 167 | reply_message = format_text_message(reserve_name + "預約兩位候補"); 168 | 169 | } 170 | // 名單超過 40 人時不新增,回傳通知訊息 171 | else { 172 | reply_message = format_text_message("⚠️ 報名額滿!已達 40 人"); 173 | } 174 | } 175 | // 非報名時間的訊息通知 176 | else { 177 | reply_message = format_text_message("現在不是報名時間喔 ~ ,請在 00:00 - 19:00 預約"); 178 | } 179 | 180 | 181 | send_to_line(); 182 | } 183 | 184 | else if (userMessage == "-1" | userMessage == "減一") { 185 | 186 | let all_members = reserve_list.getRange(1, 1, current_list_row, 1).getValues().flat(); 187 | let leaving_member_index = all_members.indexOf(reserve_name); 188 | 189 | if (leaving_member_index != -1) { 190 | let checking_range = leaving_member_index + 1; 191 | var waiting_add = reserve_list.getRange(waiting_start, 1).getValue(); 192 | 193 | reserve_list.getRange(checking_range, 1).clearContent(); 194 | current_list_row = reserve_list.getLastRow(); 195 | move_all_data(); 196 | 197 | var state = reserve_name + "已退出預約"; 198 | } 199 | else { 200 | var state = "您尚未報名,不用減一" 201 | } 202 | 203 | if (waiting_add != "") { 204 | reply_message = [{ 205 | "type": "text", 206 | "text": state 207 | }, { 208 | "type": "text", 209 | "text": waiting_add + "候補進入上課名單" 210 | }] 211 | } 212 | else { 213 | reply_message = format_text_message(state); 214 | } 215 | 216 | // 將取消報名者下方所有資料向上移動 217 | function move_all_data() { 218 | let all_members = reserve_list.getRange(1, 1, current_list_row, 1).getValues().flat(); 219 | let spaced_cell_index = all_members.indexOf(""); 220 | let modify_range = current_list_row - spaced_cell_index - 1; 221 | let tmp_data = reserve_list.getRange(spaced_cell_index + 2, 1, modify_range, 1).getValues(); 222 | 223 | reserve_list.getRange(spaced_cell_index + 1, 1, modify_range, 1).setValues(tmp_data); 224 | reserve_list.getRange(current_list_row, 1).clearContent(); 225 | } 226 | 227 | send_to_line(); 228 | } 229 | 230 | else if (userMessage == "test") { 231 | if (current_hour >= 0 & current_hour <= 19) { 232 | reply_message = [{ 233 | "type": "text", 234 | "text": "Test" 235 | }] 236 | } 237 | 238 | send_to_line(); 239 | } 240 | 241 | 242 | else if (userMessage == "報名人數" | userMessage == "名單") { 243 | var ready_namelist = "【 報名名單 】\n"; 244 | var all_members = reserve_list.getRange(1, 1, current_list_row, 1).getValues().flat(); 245 | 246 | for (var x = 0; x <= all_members.length-1; x++) { 247 | ready_namelist = ready_namelist + "\n" + all_members[x]; 248 | } 249 | reply_message = [ 250 | { 251 | "type": "text", 252 | "text": "共有 " + current_list_row + " 位同學報名 ✋" 253 | }, 254 | { 255 | "type": "text", 256 | "text": ready_namelist 257 | }] 258 | 259 | send_to_line(); 260 | } 261 | 262 | else if (userMessage == "貼圖") { 263 | reply_message = [{ 264 | "type": "sticker", 265 | "packageId": "6136", 266 | "stickerId": "10551378" 267 | }] 268 | 269 | send_to_line(); 270 | } 271 | 272 | // 其他非關鍵字的訊息則不回應( 避免干擾群組聊天室 ) 273 | else { 274 | console.log("else here,nothing will happen.") 275 | } 276 | } 277 | --------------------------------------------------------------------------------