├── 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 |
--------------------------------------------------------------------------------