diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..39e7466 Binary files /dev/null and b/.DS_Store differ diff --git a/hang_out/.gitignore b/hang_out/.gitignore new file mode 100644 index 0000000..5509140 --- /dev/null +++ b/hang_out/.gitignore @@ -0,0 +1 @@ +*.DS_Store diff --git a/hang_out/Procfile b/hang_out/Procfile new file mode 100644 index 0000000..e7aa857 --- /dev/null +++ b/hang_out/Procfile @@ -0,0 +1 @@ +web: gunicorn app_core:app –preload diff --git a/hang_out/app_core.py b/hang_out/app_core.py new file mode 100644 index 0000000..d76fe66 --- /dev/null +++ b/hang_out/app_core.py @@ -0,0 +1,216 @@ +from __future__ import unicode_literals +from flask import Flask, request, abort +import configparser +import datetime as dt + +from linebot import LineBotApi, WebhookHandler +from linebot.exceptions import InvalidSignatureError +from linebot.models import MessageEvent, TextMessage, ImageSendMessage, PostbackEvent, LocationMessage, ImageMessage + +from controllers import basic_info, text_insert, new_activity, new_registration, record, edit, verify, climate, new_page + +app = Flask(__name__) + +# LINE 聊天機器人的基本資料 +config = configparser.ConfigParser() +config.read("config.ini") + +line_bot_api = LineBotApi(config.get("line-bot", "channel_access_token")) +handler = WebhookHandler(config.get("line-bot", "channel_secret")) +mapbox_key = config.get("mapbox", "access_token") +climate_key = config.get("climate", "authorization") + +today_tw = (dt.datetime.now() + dt.timedelta(hours = 8)).date() + +# 接收 LINE 的資訊 +@app.route("/callback", methods=["POST"]) +def callback(): + signature = request.headers["X-Line-Signature"] + + body = request.get_data(as_text=True) + app.logger.info("Request body: " + body) + print(f"event_body:{body}") + + global line_id, user_condition + line_id = basic_info.line_id(request.get_data()) + user_condition = [f"line_id = '{line_id}'"] + + global user_id, init_condition + user_id = basic_info.user_id(user_condition) + init_condition = ["condition = 'initial'"] + init_condition.append(f"user_id = {user_id}" if user_id else f"user_id = Null") + + global activity_info, user_info, registration_info + activity_info, registration_info = basic_info.basic_info(init_condition) + user_info = basic_info.user_info(user_condition) + + try: + handler.handle(body, signature) + except InvalidSignatureError: + abort(400) + + return 'OK' + + +@handler.add(MessageEvent, message = TextMessage) +def message_event(event): + user_text = event.message.text + + msg = text_insert.text_insert(user_text, user_id, activity_info, user_info, registration_info, init_condition, user_condition) + + line_bot_api.reply_message( + event.reply_token, + msg + ) + +@handler.add(PostbackEvent) +def postback_event(event): + postback_data = event.postback.data + + if postback_data == "我要開團": + msg = new_activity.choose_activity_type(user_id) + + elif "開團活動類型" in postback_data: + activity_type = postback_data.split("_")[1] + + msg = new_activity.new_activity(line_id, user_id, activity_type, user_info, user_condition, init_condition) + + elif "回覆活動時間" in postback_data: + activity_date, activity_time = event.postback.params["datetime"].split("T") + + msg = new_activity.after_replay_activity_time(activity_date, activity_time, init_condition, user_info) + + elif "回覆報名截止時間" in postback_data: + due_date = event.postback.params["date"] + + msg = new_activity.after_replay_due_date(due_date, init_condition, user_info) + + elif "修改" in postback_data: + table, column = postback_data.split("_", 2)[1:] + + msg = edit.edit(table, column, init_condition, user_condition, activity_info) + + elif "確認開團" in postback_data: + msg = new_activity.confirm_new_activity(init_condition) + + elif "取消開團" in postback_data: + msg = new_activity.cancel_new_activity(init_condition) +#------------------------------------------------------------------------------------- + elif postback_data == "我要報名": + msg = new_registration.choose_activity_type(user_id) + + elif "報名活動類型" in postback_data: + activity_type = postback_data.split("_")[1] + + msg = new_registration.activity_for_registration(today_tw, activity_type) + + elif "詳細資訊" in postback_data : + activity_id = postback_data.split("_")[1] + + msg = new_registration.activity_detail(activity_id) + + elif "立即報名" in postback_data: + activity_id = postback_data.split("_")[1] + + msg = new_registration.new_registration(line_id, user_id, activity_id, user_info, user_condition, init_condition) + + elif "確認報名" in postback_data: + activity_id = postback_data.split("_")[1] + + msg = new_registration.confirm_registration(activity_id, init_condition) + + elif "取消報名" in postback_data: + registration_id, activity_id = postback_data.split('_')[1:] + + msg = new_registration.cancel_registration(registration_id, activity_id) +#------------------------------------------------------------------------------------- + elif postback_data == "我的開團": + status = "開團" + + msg = record.choose_record_type(user_id, status) + + elif "開團紀錄" in postback_data: + record_type = postback_data.split("_")[1] + + msg = record.activity_record(user_id, record_type, today_tw) + + elif "開團資訊" in postback_data: + activity_id = postback_data.split("_")[1] + + msg = record.activity_information(activity_id) + + #主揪查看報名者資訊(報名者暱稱、電話) + elif "報名者資訊" in postback_data: + activity_id = postback_data.split("_")[1] + + msg = record.attendee_information(activity_id) + + #主揪提早關團 + elif "結束報名" in postback_data: + activity_id = postback_data.split("_")[1] + + msg = record.close_activity(activity_id) +#------------------------------------------------------------------------------------- + elif postback_data == "我的報名": + status = "報名" + + msg = record.choose_record_type(user_id, status) + + elif "報名紀錄" in postback_data: + record_type = postback_data.split("_")[1] + + msg = record.registration_record(user_id, record_type, today_tw) + + elif "查報名" in postback_data: + activity_id, registration_id = postback_data.split('_')[1:] + + msg = record.activity_registration_information(activity_id, registration_id, user_condition) +#------------------------------------------------------------------------------------- + elif "newPage" in postback_data: + button_type, i, record_type = postback_data.split("_")[0:3] + i = int(i) # i代表要從資料庫中第幾個資料開始呈現,下一頁的 i=i+8,上一頁的 i=i-8 + + msg = new_page.new_page(button_type, i, record_type, today_tw, user_id) +#------------------------------------------------------------------------------------- + elif "climate" in postback_data: + activity_id = postback_data.split("_")[1] + + location_longitude, location_latitude, activity_date, activity_time = climate.location_datetime(activity_id) + + county, district = climate.geo_data(mapbox_key, location_longitude, location_latitude) + + msg = climate.climate(climate_key, county, district, activity_date, activity_time) + + line_bot_api.reply_message( + event.reply_token, + msg + ) + +@handler.add(MessageEvent, message = LocationMessage) +def location(event): + # 取得地點名稱、經度、緯度, 如果該地點沒有名稱,則用地址取代 + location_info = event.message + position, latitude, longitude, address = location_info.title, location_info.latitude, location_info.longitude, location_info.address + + msg = new_activity.after_replay_location(position, latitude, longitude, address, init_condition, user_info) + + line_bot_api.reply_message( + event.reply_token, + msg + ) + +@handler.add(MessageEvent, message = ImageMessage) +def pic(event): + photo_content = line_bot_api.get_message_content(event.message.id) + imgur_config = [config.get('imgur', 'client_id'), config.get('imgur', 'client_secret'), config.get('imgur', 'access_token'), config.get('imgur', 'refresh_token'), config.get('imgur', 'album_id')] + + msg = new_activity.after_replay_photo(photo_content, imgur_config, user_info, activity_info, init_condition, user_id) + + line_bot_api.reply_message( + event.reply_token, + msg + ) + +if __name__ == "__main__": + app.run() + \ No newline at end of file diff --git a/hang_out/controllers/basic_info.py b/hang_out/controllers/basic_info.py new file mode 100644 index 0000000..2b7461c --- /dev/null +++ b/hang_out/controllers/basic_info.py @@ -0,0 +1,28 @@ +from models import CallDatabase +from templates import flexmsg_activity +import json + +activity_columns = flexmsg_activity.columns +user_columns = ["name", "phone"] + +def line_id(request_data): + try: + line_id = json.loads(request_data)["events"][0]["source"]["userId"] + except: + line_id = None + return line_id + +def user_id(user_condition): + user_id = CallDatabase.get_data("users", ["id"], condition = user_condition, all_data = False) + user_id = user_id[0] if user_id else None + return user_id + +def basic_info(init_condition): + activity_info = CallDatabase.get_data("activity", activity_columns[:-2], condition = init_condition, all_data = False) + registration_info = CallDatabase.get_data("registration", ["activity_id", "id"], condition = init_condition, all_data = False) + return activity_info, registration_info + +def user_info(user_condition): + user_info = CallDatabase.get_data("users", user_columns, condition = user_condition, all_data = False) + return user_info + \ No newline at end of file diff --git a/hang_out/controllers/climate.py b/hang_out/controllers/climate.py new file mode 100644 index 0000000..5de5482 --- /dev/null +++ b/hang_out/controllers/climate.py @@ -0,0 +1,56 @@ +import datetime as dt +import requests +from models import CallDatabase +from templates import flexmsg_climate + +def location_datetime(activity_id): + condition = [f"id = {activity_id}"] + location_datetime = CallDatabase.get_data("activity", ["location_longitude", "location_latitude", "activity_date", "activity_time"], condition = condition, all_data = False) + return location_datetime + +def geo_data(mapbox_key, location_longitude, location_latitude): + geo_url = f"https://api.mapbox.com/geocoding/v5/mapbox.places/{location_longitude},{location_latitude}.json" + my_params = {"language": "zh-tw", "access_token": mapbox_key} + re_mapbox = requests.get(geo_url, params = my_params) + county = re_mapbox.json()["features"][0]["context"][-2]["text"] + district = re_mapbox.json()["features"][0]["context"][-3]["text"] + print(county, district) + return county, district + +def climate(climate_key, county, district, activity_date, activity_time): + try: + # climate_data + county_code = {'宜蘭縣': 'F-D0047-003', '桃園市': 'F-D0047-007', '新竹縣': 'F-D0047-011', '苗栗縣': 'F-D0047-015', '彰化縣': 'F-D0047-019', '南投縣': 'F-D0047-023', '雲林縣': 'F-D0047-027', '嘉義縣': 'F-D0047-031', '屏東縣': 'F-D0047-035', '臺東縣': 'F-D0047-039', '花蓮縣': 'F-D0047-043', '澎湖縣': 'F-D0047-047', '基隆市': 'F-D0047-051', '新竹市': 'F-D0047-055', '嘉義市': 'F-D0047-059', '臺北市': 'F-D0047-063', '高雄市': 'F-D0047-067', '新北市': 'F-D0047-071', '臺中市': 'F-D0047-075', '臺南市': 'F-D0047-079', '連江縣': 'F-D0047-083', '金門縣': 'F-D0047-087'} + climate_url = f"https://opendata.cwb.gov.tw/api/v1/rest/datastore/{county_code.get(county)}" + my_params = {"Authorization": climate_key, "locationName": district} + + re_climate = requests.get(climate_url, params = my_params).json() + weather_element = re_climate["records"]["locations"][0]["location"][0]["weatherElement"] + UVI = weather_element.pop(9) + + start_time = dt.datetime.strptime(weather_element[0]["time"][0]["startTime"], "%Y-%m-%d %H:%M:%S") + dt_list = [start_time] + [dt.datetime.strptime(time["endTime"], "%Y-%m-%d %H:%M:%S") for time in weather_element[0]["time"]] + activity_dt = dt.datetime.strptime(str(activity_date) + str(activity_time), "%Y-%m-%d%H:%M:%S") + i = 0 + while i < len(dt_list) - 1: + if dt_list[i] <= activity_dt < dt_list[i+1]: + break + else: + i += 1 + print(i) + + if i == len(dt_list) - 1: + print("僅提供一週內的天氣預報!") + return flexmsg_climate.no_climate() + + else: + climate_data = {item["description"]: list(item["time"][i]["elementValue"][0].values()) for item in weather_element} + UVI = {item["startTime"].split()[0]:[[row["value"], row["measures"]] for row in item["elementValue"]] for item in UVI["time"]} + uvi = [row[0] for row in UVI.get(str(activity_date), [])] + print(activity_dt, weather_element[0]["time"][i], climate_data, end = "\n") + + climate_lst = ["12小時降雨機率", "天氣現象", "平均溫度", "最高溫度", "最低溫度", "平均相對濕度", "風向", "最大風速"] + climate_info = [climate_data[item][0] for item in climate_lst] + [uvi] + return flexmsg_climate.climate(activity_date, county, district, climate_info) + except: + return flexmsg_climate.no_data() diff --git a/hang_out/controllers/edit.py b/hang_out/controllers/edit.py new file mode 100644 index 0000000..aec4c23 --- /dev/null +++ b/hang_out/controllers/edit.py @@ -0,0 +1,11 @@ +from models import CallDatabase +from templates import flexmsg_question + +def edit(table, column, init_condition, user_condition, activity_info): + activity_date = activity_info[2] if activity_info else None + condition = init_condition if table == "activity" else user_condition + # 將資料庫中該題的內容清除 + CallDatabase.update(table, columns = [column], values = ["Null"], condition = condition) + msg = flexmsg_question.question(column, progress = 0, activity_date = activity_date) + return msg + \ No newline at end of file diff --git a/hang_out/controllers/new_activity.py b/hang_out/controllers/new_activity.py new file mode 100644 index 0000000..1248143 --- /dev/null +++ b/hang_out/controllers/new_activity.py @@ -0,0 +1,85 @@ +from models import CallDatabase +from templates import flexmsg_activity +from controllers import reset, basic_info, verify, update_photo +from linebot.models import TextSendMessage +import datetime as dt + +def choose_activity_type(user_id): + reset.reset(user_id) + msg = flexmsg_activity.activity_type() + return msg + +def new_activity(line_id, user_id, activity_type, user_info, user_condition, init_condition): + if not user_id: + CallDatabase.insert("users", columns = ["line_id"], values = [line_id]) + user_id = CallDatabase.get_data("users", ["id"], condition = user_condition, all_data = False)[0] + init_condition = ["condition = 'initial'", f"user_id = {user_id}"] + + #創建一列(condition = initial) + columns = ["condition", "user_id", "activity_type", "attendee", "photo", "description"] + values = ["initial", user_id, activity_type, 0, "無", "無"] + CallDatabase.insert("activity", columns = columns, values = values) + + activity_info = basic_info.basic_info(init_condition)[0] + data_activity = activity_info + user_info + #回傳問題 + progress_activity = 5 if data_activity[13] else 7 #to-do 進度條待改 + msg = verify.next_msg("activity", data_activity, progress_activity) + return msg + +def after_replay_activity_time(activity_date, activity_time, init_condition, user_info): + # 預設 due_date 為 activity_date 的前一天 + due_date_default = dt.datetime.strptime(activity_date, "%Y-%m-%d") - dt.timedelta(days=1) + + columns = ["activity_date", "activity_time", "due_date"] + values = [activity_date, activity_time, due_date_default] + CallDatabase.update("activity", columns = columns, values = values, condition = init_condition) + + activity_info = basic_info.basic_info(init_condition)[0] + data_activity = activity_info + user_info + + progress_activity = 5 if data_activity[13] else 7 #to-do 進度條待改 + msg = verify.next_msg("activity", data_activity, progress_activity) + return msg + +def after_replay_due_date(due_date, init_condition, user_info): + CallDatabase.update("activity", columns = ["due_date"], values = [due_date], condition = init_condition) + activity_info = basic_info.basic_info(init_condition)[0] + data_activity = activity_info + user_info + + msg = verify.next_msg("activity", data_activity) + return msg + +def after_replay_location(position, latitude, longitude, address, init_condition, user_info): + if position == None: + position = address[:50] + + CallDatabase.update("activity", columns = ["location_title", "location_latitude", "location_longitude"], values = [position, latitude, longitude], condition = init_condition) + + activity_info = basic_info.basic_info(init_condition)[0] + data_activity = activity_info + user_info + + progress_activity = 5 if data_activity[13] else 7 #to-do 進度條待改 + msg = verify.next_msg("activity", data_activity, progress_activity) #to-do 進度條待改 + return msg + +def after_replay_photo(photo_content, imgur_config, user_info, activity_info, init_condition, user_id): + msg_text = update_photo.photo(photo_content, imgur_config, activity_info, init_condition, user_id) + msg = [TextSendMessage(text = msg_text)] + + if msg_text != "現在不用傳圖片給我!": + activity_info = basic_info.basic_info(init_condition)[0] + data_activity = activity_info + user_info + msg.append(verify.next_msg("activity", data_activity)) + + return msg + +def confirm_new_activity(init_condition): + CallDatabase.update("activity", columns = ["condition"], values = ["pending"], condition = init_condition) + msg = TextSendMessage(text = "開團成功!") + return msg + +def cancel_new_activity(init_condition): + CallDatabase.update("activity", columns = ["condition"], values = ["delete"], condition = init_condition) + msg = TextSendMessage(text = "取消成功!") + return msg diff --git a/hang_out/controllers/new_page.py b/hang_out/controllers/new_page.py new file mode 100644 index 0000000..99cfd97 --- /dev/null +++ b/hang_out/controllers/new_page.py @@ -0,0 +1,40 @@ +from models import CallDatabase +from templates import flexmsg_registration, flexmsg_record + +def new_page(button_type, i, record_type, today_tw, user_id): + all_activity_type = ["登山踏青", "桌遊麻將", "吃吃喝喝", "唱歌跳舞"] + + # [我要報名] 可報名列表的上下頁 + if button_type in all_activity_type: + # 篩選可以報名的團 + condition = [f"due_date >= '{today_tw}'", f"activity_type = '{button_type}'", "people > attendee", "condition = 'pending'"] + data_carousel = CallDatabase.get_data("activity", condition = condition, order = "activity_date") + # 回傳 下一頁或上一頁的列表 + msg = flexmsg_registration.carousel(data_carousel, button_type, i) + + # [我的開團]、[我的報名] 列表的下一頁 + else: + condition = ["condition IN ('closed', 'closed by owner', 'pending')", f"user_id = '{user_id}'"] + + # 我的開團紀錄 + if record_type == "開團紀錄": + condition.append(f"activity_date < '{today_tw}'") if button_type == "已結束" else condition.append(f"activity_date >= '{today_tw}'") + activity_info = CallDatabase.get_data("activity", ["id", "activity_name"], condition = condition, order = "activity_date", all_data = True) + msg = flexmsg_record.record_list("開團", activity_info, button_type, i) + + # 我的報名紀錄 + elif record_type == "報名紀錄": + # 取得報名紀錄 + registration_data = CallDatabase.get_data("registration", ["activity_id", "id"], condition = condition, all_data = True) + all_activity_id = [data[0] for data in registration_data] + activity_name_date = [CallDatabase.get_data("activity", ["activity_name", "activity_date"], condition = [f"id = {activity_id}"], all_data = False) for activity_id in all_activity_id] + registration_data = [data[0] + list(data[1]) for data in zip(registration_data, activity_name_date)] # activity_id, registration_id, activity_name + if button_type == "已結束": + registration_data = [data for data in registration_data if data[3] < today_tw] + else: + registration_data = [data for data in registration_data if data[3] >= today_tw] + registration_data = sorted(registration_data, key = lambda x:x[3]) + # 回傳回傳報名列表 + msg = flexmsg_record.record_list("報名", registration_data, button_type, i) + + return msg diff --git a/hang_out/controllers/new_registration.py b/hang_out/controllers/new_registration.py new file mode 100644 index 0000000..214cdc8 --- /dev/null +++ b/hang_out/controllers/new_registration.py @@ -0,0 +1,106 @@ +from models import CallDatabase +from templates import flexmsg_registration +from controllers import reset, basic_info, verify +from linebot.models import TextSendMessage + + +def choose_activity_type(user_id): + reset.reset(user_id) + msg = flexmsg_registration.activity_type() + return msg + +def activity_for_registration(today_tw, activity_type): + # 篩選可以報名的團 + condition = [f"due_date >= '{today_tw}'", f"activity_type = '{activity_type}'", "people > attendee", "condition = 'pending'"] + data_carousel = CallDatabase.get_data("activity", condition = condition, order = "activity_date") + + #傳可報名的團的選單 + msg = flexmsg_registration.carousel(data_carousel, activity_type) + return msg + +def activity_detail(activity_id): + #根據該活動的編號找出活動資訊 + condition = [f"id = {activity_id}"] + activity_info = basic_info.basic_info(condition)[0] + owner_user_id = CallDatabase.get_data("activity", ["user_id"], condition = condition, all_data = False)[0] + + condition = [f"id = {owner_user_id}"] + owner_user_info = CallDatabase.get_data("users", ["name", "phone"], condition = condition, all_data = False) + + activity_info = activity_info + owner_user_info + + # 回傳該團活動資訊 + msg = flexmsg_registration.more_info(activity_info, activity_id) + return msg + +def new_registration(line_id, user_id, activity_id, user_info, user_condition, init_condition): + + if user_id and verify.duplicate_registration(activity_id, user_id): + print("報名重複") + msg = flexmsg_registration.duplication() + + else: + if not user_id: + CallDatabase.insert("users", columns = ["line_id"], values = [line_id]) + user_id = CallDatabase.get_data("users", ["id"], condition = user_condition, all_data = False)[0] + init_condition = ["condition = 'initial'", f"user_id = {user_id}"] + + #在報名資訊資料庫創建一列,準備寫入使用者回答的資訊該列報名狀態為initial + columns = ["activity_id", "condition", "user_id"] + values = [activity_id, "initial", user_id] + CallDatabase.insert("registration", columns = columns, values = values) + + registration_info = basic_info.basic_info(init_condition)[1] + data_registration = user_info + registration_info + + msg = verify.next_msg("registration", data_registration, progress = 2) #to-do 進度條待改 + return msg + +def confirm_registration(activity_id, init_condition): + condition = [f"id = {activity_id}"] + people, activity_condition = CallDatabase.get_data("activity", ["people", "condition"], condition = condition, all_data = False) + + if activity_condition != "pending": + msg = TextSendMessage(text = "報名失敗!") + + else: + #將報名表單的condition改成closed + CallDatabase.update("registration", columns = ["condition"], values = ["closed"], condition = init_condition) + + # 報名成功 重新取得報名人數 + condition = [f"activity_id = {activity_id}", f"condition = 'closed'"] + count_attendee = len(CallDatabase.get_data("registration", condition = condition)) + + #檢查報名人數attendee是否達上限people(上面有先從資料庫取出people了) + #人數達上限則將活動狀態改為closed + condition = [f"id = {activity_id}"] + if count_attendee >= people: + CallDatabase.update("activity", columns = ["condition"], values = ["closed"], condition = condition) + + #將更新的報名人數attendee記錄到報名表單group_data裡 + CallDatabase.update("activity", columns = ["attendee"], values = [count_attendee], condition = condition) + + # 回應 報名成功 + msg = TextSendMessage(text = "報名成功!") + return msg + +def cancel_registration(registration_id, activity_id): + # 從報名資訊資料庫中,將該報名紀錄的 condition 改為 delete + condition = [f"id = {registration_id}"] + CallDatabase.update("registration", columns = ["condition"], values = ["delete"], condition = condition) + + # 更新活動資訊資料庫中,該活動已報名人數 + condition = [f"activity_id = {activity_id}", f"condition = 'closed'"] + count_attendee = len(CallDatabase.get_data("registration", condition = condition)) + condition = [f"id = {activity_id}"] + CallDatabase.update("activity", columns = ["attendee"], values = [count_attendee], condition = condition) + + # 若該團的活動狀態為closed,則更新為pending + activity_condition = CallDatabase.get_data("activity", ["condition"], condition = condition, all_data = False)[0] + if activity_condition == "closed": + CallDatabase.update("activity", columns = ["condition"], values = ["pending"], condition = condition) + + # 回應 取消成功 + msg = TextSendMessage(text = "取消成功!") + return msg + \ No newline at end of file diff --git a/hang_out/controllers/record.py b/hang_out/controllers/record.py new file mode 100644 index 0000000..f4cd9c6 --- /dev/null +++ b/hang_out/controllers/record.py @@ -0,0 +1,88 @@ +from models import CallDatabase +from templates import flexmsg_record +from controllers import reset +from linebot.models import TextSendMessage + +def choose_record_type(user_id, status): + reset.reset(user_id) + msg = flexmsg_record.record_type(status) + return msg + +def activity_record(user_id, record_type, today_tw): + # 依照活動時間篩選為已結束或進行中 + if user_id: + condition = ["condition <> 'initial'", f"user_id = '{user_id}'"] + condition.append(f"activity_date < '{today_tw}'") if record_type == "已結束" else condition.append(f"activity_date >= '{today_tw}'") + + # 取得開團紀錄 + activity_info = CallDatabase.get_data("activity", ["id", "activity_name"], condition = condition, order = "activity_date", all_data = True) + msg = flexmsg_record.record_list("開團", activity_info, record_type) + else: + msg = flexmsg_record.record_list("開團", None, record_type) + return msg + +def registration_record(user_id, record_type, today_tw): + # 依照活動時間篩選為已結束或進行中 + if user_id: + condition = ["condition = 'closed'", f"user_id = '{user_id}'"] + + # 取得報名紀錄 + registration_data = CallDatabase.get_data("registration", ["activity_id", "id"], condition = condition, all_data = True) + all_activity_id = [data[0] for data in registration_data] + activity_name_date = [CallDatabase.get_data("activity", ["activity_name", "activity_date"], condition = [f"id = {activity_id}"], all_data = False) for activity_id in all_activity_id] + registration_data = [data[0] + list(data[1]) for data in zip(registration_data, activity_name_date)] # activity_id, registration_id, activity_name + if record_type == "已結束": + registration_data = [data for data in registration_data if data[3] < today_tw] + else: + registration_data = [data for data in registration_data if data[3] >= today_tw] + registration_data = sorted(registration_data, key = lambda x:x[3]) + # 回傳回傳報名列表 + print(registration_data) # activity_id, registration_id, activity_name, activity_date + msg = flexmsg_record.record_list("報名", registration_data, record_type) + else: + msg = flexmsg_record.record_list("報名", None, record_type) + return msg + +def activity_information(activity_id): + condition = [f"id = {activity_id}"] + activity_info = CallDatabase.get_data("activity", condition = condition, all_data = False) + + # 回傳活動資訊 + msg = flexmsg_record.send_activity_info(activity_info) + return msg + +def attendee_information(activity_id): + condition = [f"id = {activity_id}"] + activity_name = CallDatabase.get_data("activity", ["activity_name"], condition = condition, all_data = False)[0] + + # 取得該團的報名資訊 + condition = [f"activity_id = {activity_id}", f"condition = 'closed'"] + all_attendee_id = [data[0] for data in CallDatabase.get_data("registration", ["user_id"], condition = condition)] + attendee_data = [CallDatabase.get_data("users", ["name", "phone"], condition = [f"id = {attendee_id}"], all_data = False) for attendee_id in all_attendee_id] + + msg = flexmsg_record.attendee_info(activity_name, attendee_data) + return msg + +def activity_registration_information(activity_id, registration_id, user_condition): + # 根據回傳的 activity_id,從 activity 裡找到活動資訊 + condition = [f"id = {activity_id}"] + activity_data = CallDatabase.get_data("activity", condition = condition, all_data = False) + + # 取得 owner 資料 + condition = [f"id = {activity_data[15]}"] + owner_info = CallDatabase.get_data("users", ["name", "phone"], condition = condition, all_data = False) + + # 取得 user 資料 + user_info = CallDatabase.get_data("users", ["name", "phone"], condition = user_condition, all_data = False) + + # 回傳活動資訊及報名資訊 + msg = flexmsg_record.activity_registration_info(activity_data, owner_info, user_info, registration_id) + return msg + +def close_activity(activity_id): + condition = [f"id = {activity_id}"] + CallDatabase.update("activity", columns = ["condition"], values = ["closed by owner"], condition = condition) + + # 回應 成功結束報名 + msg = TextSendMessage(text = "成功結束報名!") + return msg diff --git a/hang_out/controllers/reset.py b/hang_out/controllers/reset.py new file mode 100644 index 0000000..f23e95b --- /dev/null +++ b/hang_out/controllers/reset.py @@ -0,0 +1,10 @@ +from models import CallDatabase + +def reset(user_id): + if user_id: + init_condition = ["condition = 'initial'", f"user_id = '{user_id}'"] + + CallDatabase.delete("activity", condition = init_condition) # 刪除開團表單中未完成的資料 + CallDatabase.delete("registration", condition = init_condition) # 刪除報名表單中未完成的資料 + + print("刪除未成功資料") diff --git a/hang_out/controllers/text_insert.py b/hang_out/controllers/text_insert.py new file mode 100644 index 0000000..2cf14d3 --- /dev/null +++ b/hang_out/controllers/text_insert.py @@ -0,0 +1,42 @@ +from models import CallDatabase +from templates import flexmsg_activity +from controllers import verify, basic_info +from linebot.models import TextSendMessage +import psycopg2 + +activity_columns = flexmsg_activity.columns +user_columns = ["name", "phone"] + +def text_insert(user_text, user_id, activity_info, user_info, registration_info, init_condition, user_condition): + msg = TextSendMessage(text = "點開下方選單,開始揪團!") + + # to-do 隱藏功能,未來可刪 + if user_text == "~cancel": + reset.reset(user_id) + msg = TextSendMessage(text = "取消成功") + + elif activity_info or registration_info: + if activity_info and (None in activity_info): + table, columns, condition = "activity", activity_columns[activity_info.index(None)], init_condition + progress = 5 if user_info[1] else 7 + elif user_info and (None in user_info): + table, columns, condition = "users", user_columns[user_info.index(None)], user_condition + progress = 2 if not activity_info else 7 + + try: + CallDatabase.update(table, columns = [columns], values = [user_text], condition = condition) + activity_info = basic_info.basic_info(init_condition)[0] + user_info = basic_info.user_info(user_condition) + data_activity = activity_info + user_info if activity_info and user_info else None + data_registration = user_info + registration_info if user_info and registration_info else None + + status, data = ["activity", data_activity] if activity_info else ["registration", data_registration] + msg = verify.next_msg(status, data, progress) + + except psycopg2.DataError: + DATABASE_URL = "postgres://kfcgjdnbgqalif:503a165ffdfaed9dde93a0196045fe9db0ea2464206a1c129af5580932ccdab5@ec2-54-227-246-76.compute-1.amazonaws.com:5432/d107jas11a6r56" + conn = psycopg2.connect(DATABASE_URL, sslmode='require') + cursor = conn.cursor() + msg = TextSendMessage(text = "請重新輸入") + + return msg diff --git a/hang_out/controllers/update_photo.py b/hang_out/controllers/update_photo.py new file mode 100644 index 0000000..7229651 --- /dev/null +++ b/hang_out/controllers/update_photo.py @@ -0,0 +1,43 @@ +import os +from models import CallDatabase +from imgurpython import ImgurClient + +def photo(photo_content, imgur_config, activity_info, init_condition, user_id): + if activity_info and None in activity_info and activity_info.index(None) == 11: + activity_name = activity_info[1] + #把圖片存下來並傳上去 + file_path = f"tmp/{user_id}_{activity_name}.png" + with open(file_path, "wb") as tf: + for chunk in photo_content.iter_content(): + tf.write(chunk) + dist_path = tf.name + + print(f"dist_path = {dist_path}") + + try: + client_id, client_secret, access_token, refresh_token, album_id = imgur_config + client = ImgurClient( client_id, client_secret, access_token, refresh_token) + con = { + 'album': album_id, + 'name': f'{user_id} / {activity_name}', + 'title': f'{user_id} / {activity_name}', + 'description': f'{user_id} / {activity_name}' + } + + image = client.upload_from_path(dist_path, config = con, anon = False) + os.remove(dist_path) + #把圖片網址存進資料庫 + CallDatabase.update("activity", columns = ["photo"], values = [image['link']], condition = init_condition) + print(image['link']) + + msg = "上傳成功!" + + except: + CallDatabase.update("activity", columns = ["photo"], values = ["無"], condition = init_condition) + msg = "上傳失敗!" + + else: + msg = "現在不用傳圖片給我!" + + return msg + \ No newline at end of file diff --git a/hang_out/controllers/verify.py b/hang_out/controllers/verify.py new file mode 100644 index 0000000..cdc8d54 --- /dev/null +++ b/hang_out/controllers/verify.py @@ -0,0 +1,37 @@ +from models import CallDatabase +from templates import flexmsg_activity, flexmsg_registration, flexmsg_question + +activity_columns = flexmsg_activity.columns +user_columns = ["name", "phone"] + +# 驗證是否回答完畢,判斷要問下一題或summary +def next_msg(status, data, progress = 0): + print("進入verify.next_msg") + if None in data: # 還有欄位未填,問下一題 + i = data.index(None) + if "activity" in status: + col = activity_columns[i] + activity_date = data[2] + msg = flexmsg_question.question(col, progress, activity_date) + else: + col = user_columns[i] + msg = flexmsg_question.question(col, progress) + print("問下一題") + + else: # 所有欄位都填了,回傳summary + if "activity" in status: + msg = flexmsg_activity.summary(data) + else: + activity_id = data[2] + activity_name = CallDatabase.get_data("activity", ["activity_name"], condition = [f"id = {activity_id}"], all_data = False)[0] + msg = flexmsg_registration.summary(data, activity_name) + + return msg + +# 驗證使用者報名是否重複 +def duplicate_registration(activity_id, user_id): + condition = [f"activity_id = {activity_id}", "condition = 'closed'"] + attendee_user_id = CallDatabase.get_data("registration", ["user_id"], condition = condition) + attendee_user_id = [data[0] for data in attendee_user_id] + + return user_id in attendee_user_id #如果使用者輸入的電話重複則報名失敗 diff --git a/hang_out/models/CallDatabase.py b/hang_out/models/CallDatabase.py new file mode 100644 index 0000000..5040933 --- /dev/null +++ b/hang_out/models/CallDatabase.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +import os +import psycopg2 +import datetime as dt +now_timestamp = dt.datetime.now().timestamp() + +# 連接資料庫 +# DATABASE_URL = os.environ["DATABASE_URL"] +DATABASE_URL = "postgres://kfcgjdnbgqalif:503a165ffdfaed9dde93a0196045fe9db0ea2464206a1c129af5580932ccdab5@ec2-54-227-246-76.compute-1.amazonaws.com:5432/d107jas11a6r56" +conn = psycopg2.connect(DATABASE_URL, sslmode='require') +cursor = conn.cursor() + +def get_data(table, columns = "*", condition = None, order = None, ASC = True, all_data = True): + + default_photo = "https://scdn.line-apps.com/n/channel_devcenter/img/flexsnapshot/clip/clip11.jpg" + + # 處理查詢欄位 + columns = ",".join(columns) + # 處理篩選條件 + condition_query = "WHERE " + " AND ".join(condition) if condition else "" + # 處理排序 + order_query = f" ORDER BY {order}" + (" ASC" if ASC else " DESC") if order else "" + + postgres_select_query = f"""SELECT {columns} FROM {table} {condition_query} {order_query}""" + + cursor.execute(postgres_select_query) + conn.commit + + # 選取資料 + data = [list(row) for row in cursor.fetchall()] if all_data else cursor.fetchone() + return data + +def insert(table, columns, values): + + columns.append("created_at") + values.append(now_timestamp) + + columns = ",".join([f"{col}" for col in columns]) + values = ",".join([f"'{val}'" for val in values]) + postgres_insert_query = f"""INSERT INTO {table} ({columns}) VALUES ({values})""" + + cursor.execute(postgres_insert_query) + conn.commit() + +def update(table, columns, values, condition): + + columns = ",".join([f"{col}" for col in columns]) + values = ",".join([f"'{val}'" for val in values]) if values != ["Null"] else "Null" + condition_query = "WHERE " + " AND ".join(condition) + + if "," not in columns: + postgres_update_query = f"""UPDATE {table} SET {columns} = {values} {condition_query}""" + else: + postgres_update_query = f"""UPDATE {table} SET ({columns}) = ({values}) {condition_query}""" + + cursor.execute(postgres_update_query) + conn.commit() + + time_query = f"""UPDATE {table} SET updated_at = {now_timestamp} {condition_query}""" + cursor.execute(time_query) + conn.commit() + +def delete(table, condition): + + condition_query = " WHERE " + " AND ".join(condition) + postgres_delete_query = f"""DELETE FROM {table} {condition_query}""" + + cursor.execute(postgres_delete_query) + conn.commit() diff --git a/hang_out/models/create_table.py b/hang_out/models/create_table.py new file mode 100644 index 0000000..baf1ee8 --- /dev/null +++ b/hang_out/models/create_table.py @@ -0,0 +1,63 @@ +import os +import psycopg2 + +DATABASE_URL = os.popen('heroku config:get DATABASE_URL -a chatbot-ccclub').read()[:-1] +conn = psycopg2.connect(DATABASE_URL, sslmode='require') +cursor = conn.cursor() + +#創開團表單 activity +create_table = '''CREATE TABLE activity( + id serial PRIMARY KEY, + activity_type VARCHAR (50), + activity_name VARCHAR (50), + activity_date DATE , + activity_time TIME , + location_title VARCHAR (50), + location_latitude NUMERIC (9, 6) , + location_longitude NUMERIC (9, 6), + people INTEGER , + cost INTEGER , + due_date DATE , + description TEXT, + photo TEXT, + attendee INTEGER , + condition VARCHAR (50), + user_id INTEGER, + created_at VARCHAR (50), + updated_at VARCHAR (50) + );''' + +cursor.execute(create_table) +conn.commit() + +#創報名表單 registration +create_table = '''CREATE TABLE registration( + id serial PRIMARY KEY, + activity_id INTEGER, + condition VARCHAR (50), + user_id INTEGER, + created_at VARCHAR (50), + updated_at VARCHAR (50) + );''' + +cursor.execute(create_table) +conn.commit() + + +#創用戶表單 users +create_table = '''CREATE TABLE users( + id serial PRIMARY KEY, + name VARCHAR (50), + phone VARCHAR (50), + line_id VARCHAR (50), + created_at VARCHAR (50), + updated_at VARCHAR (50) + );''' + +cursor.execute(create_table) +conn.commit() + + + +cursor.close() +conn.close() diff --git a/hang_out/requirements.txt b/hang_out/requirements.txt new file mode 100644 index 0000000..6ec1b1e --- /dev/null +++ b/hang_out/requirements.txt @@ -0,0 +1,6 @@ +Flask==1.1.1 +gunicorn==19.9.0 +line-bot-sdk==1.18.0 +psycopg2==2.8.3 +datetime==4.0 +imgurpython==1.1.7 diff --git a/hang_out/runtime.txt b/hang_out/runtime.txt new file mode 100644 index 0000000..43b47fb --- /dev/null +++ b/hang_out/runtime.txt @@ -0,0 +1 @@ +python-3.8.5 diff --git a/hang_out/static/climate/climate_cloud.png b/hang_out/static/climate/climate_cloud.png new file mode 100644 index 0000000..aed19be Binary files /dev/null and b/hang_out/static/climate/climate_cloud.png differ diff --git a/hang_out/static/climate/climate_rain.png b/hang_out/static/climate/climate_rain.png new file mode 100644 index 0000000..b6fa289 Binary files /dev/null and b/hang_out/static/climate/climate_rain.png differ diff --git a/hang_out/static/climate/climate_storm_rain.png b/hang_out/static/climate/climate_storm_rain.png new file mode 100644 index 0000000..97da6b5 Binary files /dev/null and b/hang_out/static/climate/climate_storm_rain.png differ diff --git a/hang_out/static/climate/climate_sunny.png b/hang_out/static/climate/climate_sunny.png new file mode 100644 index 0000000..935c3f5 Binary files /dev/null and b/hang_out/static/climate/climate_sunny.png differ diff --git a/hang_out/static/climate/climate_sunny_cloud.png b/hang_out/static/climate/climate_sunny_cloud.png new file mode 100644 index 0000000..20b0e15 Binary files /dev/null and b/hang_out/static/climate/climate_sunny_cloud.png differ diff --git a/hang_out/static/climate/climate_sunny_rain.png b/hang_out/static/climate/climate_sunny_rain.png new file mode 100644 index 0000000..c2d3f28 Binary files /dev/null and b/hang_out/static/climate/climate_sunny_rain.png differ diff --git a/hang_out/static/richmenu.jpg b/hang_out/static/richmenu.jpg new file mode 100644 index 0000000..877026d Binary files /dev/null and b/hang_out/static/richmenu.jpg differ diff --git a/hang_out/templates/flexmsg_activity.py b/hang_out/templates/flexmsg_activity.py new file mode 100644 index 0000000..c433b87 --- /dev/null +++ b/hang_out/templates/flexmsg_activity.py @@ -0,0 +1,123 @@ +from linebot.models import * + +columns = ["activity_type", "activity_name", "activity_date", "activity_time", + "location_title", "location_latitude", "location_longitude", "people", "cost", "due_date", + "description", "photo", "name", "phone"] +columns_text = ["活動類型", "活動名稱", "活動時間", "活動時間", + "活動地點", "活動地點", "活動地點", "活動人數", "活動費用", "報名截止日期", + "活動敘述", "活動相片", "主揪姓名", "主揪電話"] + +#------------------------------------------------------------------------------------- +def quick_reply_button(activity_type): + quick_reply_button = QuickReplyButton( + action = PostbackAction( + label = activity_type, + data = f"開團活動類型_{activity_type}", + display_text = activity_type + ) + ) + return quick_reply_button + +def activity_type(): + all_type = ["登山踏青", "桌遊麻將", "吃吃喝喝", "唱歌跳舞"] + activity_type = TextSendMessage( + text = "請選擇開團活動類型", + quick_reply = QuickReply( + items = [quick_reply_button(activity_type) for activity_type in all_type] + ) + ) + return activity_type +#---------------------------------------------------------------------- +def content_edit(info, text, col, action = None, color = "#141414"): + content_edit = BoxComponent( + layout = "horizontal", + margin = "lg", + contents = [ + TextComponent( + text = f"{text}:{info}", + size = "md", + flex = 6, + action = action, + color = color, + wrap = True, + ), + SeparatorComponent(), + TextComponent( + text = "修改", + size = "md", + flex = 1, + align = "end", + gravity = "top", + color = "#229C8F", + action = PostbackAction( + label = f"修改{text}", + data = f"修改_activity_{col}" if col not in ["name", "phone"] else f"修改_users_{col}", + display_text = f"修改{text}" + ) + ) + ] + ) + return content_edit + +def summary_content(data): + activity_dt = f"{data[2]} {str(data[3])[:5]}" + + summary_content = [] + for i in [0, 1, 2, 4, 7, 8, 9, 10, 11, 12, 13]: + info, text, col, action, color = data[i], columns_text[i], columns[i], None, "#141414" + if i == 3: + info = activity_dt + if i == 11 and data[11] != "無": + info, action, color = "點我查看圖片", URIAction(uri = f"{data[11]}"), "#229C8F" + summary_content.append(content_edit(info, text, col, action, color)) + + return summary_content + +def summary_button(text): + button = ButtonComponent( + style = "link", + height = "sm", + margin = "none", + color = "#229C8F", + gravity = "bottom", + action = PostbackAction( + label = text, + data = text, + display_text = text + ) + ) + return button + +def summary(data): + summary = FlexSendMessage( + alt_text = "請確認開團資訊", + contents = BubbleContainer( + direction = "ltr", + header = BoxComponent( + layout = "vertical", + contents = [ + TextComponent( + text = "請確認開團資訊:", + weight = "bold", + size = "md", + align = "start", + color = "#000000" + ) + ] + ), + body = BoxComponent( + layout = "vertical", + contents = summary_content(data) + ), + footer = BoxComponent( + layout = "horizontal", + contents = [ + summary_button("確認開團"), + SeparatorComponent(), + summary_button("取消開團") + ] + ) + ) + ) + return summary + \ No newline at end of file diff --git a/hang_out/templates/flexmsg_climate.py b/hang_out/templates/flexmsg_climate.py new file mode 100644 index 0000000..5d01193 --- /dev/null +++ b/hang_out/templates/flexmsg_climate.py @@ -0,0 +1,203 @@ +from linebot.models import * + +def climate(activity_date, county, district, climate_info): + rain, weather, temperature_avg, temperature_max, temperature_min, humidity, wind_d, wind_v, uvi = climate_info + rain_prob = f"降雨機率:{rain}%" if rain != " " else "無降雨機率資料" + weather_size = "xl" if len(weather) > 4 else "31px" + + if "晴" in weather and "雨" in weather: + image = "https://i.imgur.com/jM4qYAq.png" + elif "雷雨" in weather: + image = "https://i.imgur.com/tEXBJwC.png" + elif "雨" in weather: + image = "https://i.imgur.com/NULZt0V.png" + elif "晴" in weather and ("陰" in weather or "多雲" in weather): + image = "https://i.imgur.com/M0N0JSk.png" + elif "晴" in weather: + image = "https://i.imgur.com/Rjogm0N.png" + else: + image = "https://i.imgur.com/BoseTDT.png" + + bubble = BubbleContainer( + size = "kilo", + direction = "ltr", + header = BoxComponent( + layout = "vertical", + background_color = "#A7D5E1", + contents = [ + TextComponent( + text = "天氣預報", + size = "lg", + align = "start", + color = "#ffffff" + ) + ] + ), + body = BoxComponent( + layout = "vertical", + contents = [ + BoxComponent( + layout = "horizontal", + spacing = "xl", + contents = [ + BoxComponent( + layout = "vertical", + flex = 6, + contents = [ + TextComponent( + text = f"{county} {district}", + color = "#aaaaaa", + size = "lg", + flex = 1, + align = "start" #offset_top = "5px" + ), + TextComponent( + text = rain_prob, + color = "#aaaaaa", + size = "md", + flex = 1, + align = "end", + offset_top = "5px" + ), + TextComponent( + text = weather, + wrap = True, + size = weather_size, + flex = 3, + align = "end", + offset_top = "sm" + ), + ] + ), + BoxComponent( + layout = "vertical", + spacing = "sm", + flex = 4, + contents = [ + ImageComponent( + url = image, + size = "5xl" + ) + ] + ) + ] + ), + BoxComponent( + layout = "horizontal", + margin = "md", + contents = [ + TextComponent( + flex = 3, + text = f" {temperature_avg}ºC", + size = "xxl" + ), + TextComponent( + flex = 6, + text = f" 最高{temperature_max}ºC/最低{temperature_min}ºC", + offset_top = "lg" + ) + ] + ), + BoxComponent( + layout = "vertical", + contents = [ + SeparatorComponent( + margin = "xs" + ) + ] + ), + BoxComponent( + layout = "vertical", + contents = [ + TextComponent( + text = f"{activity_date}", + color = "#8c8c8c", + margin = "xl", + size = "md" + ), + TextComponent( + text = f"相對濕度: {humidity} %", + color = "#8c8c8c", + margin = "sm", + size = "sm" + ), + TextComponent( + text = f"紫外線指數: {uvi[0]} ({uvi[1]})", + color = "#8c8c8c", + margin = "sm", + size = "sm" + ), + TextComponent( + text = f"風向: {wind_d}", + color = "#8c8c8c", + margin = "sm", + size = "sm" + ), + TextComponent( + text = f"最大風速: {wind_v} m/s", + color = "#8c8c8c", + margin = "sm", + size = "sm" + ) + ] + ) + ] + ) + ) + + climate = FlexSendMessage( + alt_text = "天氣預報", + contents = bubble + ) + return climate + +def no_climate(): + + bubble = BubbleContainer( + direction = "ltr", + body = BoxComponent( + size = "xs", + layout = "vertical", + spacing = "md", + contents = [ + TextComponent( + text = "僅提供未來一週內的天氣預報", + size = "lg", + weight = "bold", + color = "#AAAAAA" + ) + ] + ) + ) + + no_climate = FlexSendMessage( + alt_text = "僅提供一週內的天氣預報!", + contents = bubble + ) + return no_climate + +def no_data(): + + bubble = BubbleContainer( + direction = "ltr", + body = BoxComponent( + size = "xs", + layout = "vertical", + spacing = "md", + contents = [ + TextComponent( + text = "無資料", + size = "lg", + weight = "bold", + color = "#AAAAAA" + ) + ] + ) + ) + + no_data = FlexSendMessage( + alt_text = "無資料", + contents = bubble + ) + return no_data + \ No newline at end of file diff --git a/hang_out/templates/flexmsg_question.py b/hang_out/templates/flexmsg_question.py new file mode 100644 index 0000000..7a3a400 --- /dev/null +++ b/hang_out/templates/flexmsg_question.py @@ -0,0 +1,122 @@ +from linebot.models import * +columns = ["activity_type", "activity_name", "activity_date", "activity_time", + "location_title", "location_latitude", "location_longitude", "people", "cost", "due_date", + "description", "photo", "name", "phone"] +columns_text = ["活動類型", "活動名稱", "活動時間", "活動時間", + "活動地點", "活動地點", "活動地點", "活動人數", "活動費用", "報名截止日期", + "活動敘述", "活動相片", "姓名", "電話"] + +def flex_header(text): + flex_header = BoxComponent( + layout = "vertical", + height = "63px", + contents = [ + TextComponent( + text = text, + weight = "bold", + size = "lg", + align = "center" + ) + ] + ) + return flex_header + +def flex_body(text): + flex_body = BoxComponent( + layout = "vertical", + contents = [ + TextComponent( + text = f"請告訴我{text}" if text != "活動相片" else "請上傳一張相片", + size = "md", + color = "#666666" + ) + ] + ) + return flex_body + +def prog_rate(col, q_count): + if q_count == 2: + progress_i = {"name": 1, "phone": 2} + else: + progress_i = {"activity_name": 1, "activity_date": 2, "location_title":3, "people": 4, "cost": 5, "name": 6, "phone": 7} + i = progress_i.get(col, 0) + + prog_rate = BoxComponent( + layout = "vertical", + margin = "md", + height = "45px", + contents = [ + TextComponent( + text = f"{i} / {q_count} ", + weight = "bold", + size = "md" + ), + BoxComponent( + layout = "vertical", + margin = "md", + width = f"{int(i / q_count * 100 + 0.5 )}%", + background_color = "#3DE1D0", + height = "6px" + ) + ] + ) + return prog_rate + +def button(col, activity_date = None): + if col == "activity_date": + action = DatetimePickerAction( + label = "點我選時間", + data = "回覆活動時間", + mode = "datetime" + ) + + elif col == "location_title": + action = URIAction( + label = "點我選地點", + uri = "https://line.me/R/nv/location" + ) + + elif col == "due_date": + action = DatetimePickerAction( + label = "點我選時間", + data = "回覆報名截止時間", + mode = "date", + max = str(activity_date) + ) + + button = ButtonComponent( + action = action, + height = "sm", + margin = "md", + style = "primary", + color = "#A7D5E1", + gravity = "bottom" + ) + return button + + +def question(col, progress = 0, activity_date = None): + + text = columns_text[columns.index(col)] + + footer_contents = [] + if progress: + footer_contents.append(prog_rate(col, progress)) + if col in ["activity_date", "location_title", "due_date"]: + footer_contents.append(button(col, activity_date)) + + question = FlexSendMessage( + alt_text = f"請告訴我{text}", + contents = BubbleContainer( + direction = "ltr", + header = flex_header(text), + body = flex_body(text), + footer = BoxComponent( + layout = "vertical", + margin = "md", + contents = footer_contents + ) + ) + ) + return question + \ No newline at end of file diff --git a/hang_out/templates/flexmsg_record.py b/hang_out/templates/flexmsg_record.py new file mode 100644 index 0000000..8e85fb7 --- /dev/null +++ b/hang_out/templates/flexmsg_record.py @@ -0,0 +1,329 @@ +from linebot.models import * + +def quick_reply_button(status, label, record_type): + quick_reply_button = QuickReplyButton( + action = PostbackAction( + label = label, + data = f"{status}紀錄_{record_type}", + display_text = f"查詢[{label}]" + ) + ) + return quick_reply_button + +def record_type(status): + all_label = [f"歷史{status}紀錄", "即將來臨的活動"] + all_type = ["已結束", "進行中"] + + record_type = TextSendMessage( + text = f"請選擇查詢 [歷史{status}紀錄] 或 [即將來臨的活動]", + quick_reply = QuickReply( + items = [quick_reply_button(status, label, record_type) for label, record_type in zip(all_label, all_type)] + ) + ) + return record_type +#------------------------------------------------------------------------------------------ +def activity_in_index(status, row): + if status == "開團": + activity_id, activity_name = row + else: + activity_id, registration_id, activity_name, activity_date = row + + activity_in_index = BoxComponent( + layout = "baseline", + contents = [ + IconComponent( + url = "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png", + size = "sm" + ), + TextComponent( + text = f"{activity_name}", # activity_name + align = "start", + size = "md", + color = "#227C9D", + margin = "md", + action = PostbackAction( + display_text = f"查看 {activity_name} 的詳細資訊", + data = f"開團資訊_{activity_id}" if status == "開團" else f"查報名_{activity_id}_{registration_id}" + ) + ) + ] + ) + return activity_in_index + +def page_button(status, new_page, record_type, i): + i = i-8 if new_page == "上一頁" else i+8 + page_button = ButtonComponent( + action = PostbackAction( + label = new_page, + data = f"newPage_{record_type}_{i}_{status}紀錄", + display_text = new_page + ), + height = "sm", + style = "primary", + color = "#A7D5E1", + gravity = "bottom" + ) + return page_button + +def record_list(status, data, record_type, i = 0): + + if data: + if i < 0: + i = 0 + elif i >= len(data): + i -= 8 + + index_content = [activity_in_index(status, row) for row in data[i: i+8]] + index = BubbleContainer( + size = "kilo", + direction = "ltr", + header = BoxComponent( + layout = "horizontal", + contents = [ + TextComponent( + text = f"我的{status}列表 ({record_type})", + size = "lg", + weight = "bold", + color = "#AAAAAA" + ) + ] + ), + body = BoxComponent( + layout = "vertical", + spacing = "md", + contents = index_content + ), + footer = BoxComponent( + layout = "horizontal", + spacing = "sm", + contents = [ + page_button(status, "上一頁", record_type, i), + page_button(status, "下一頁", record_type, i) + ] + ) + ) + else: + index = BubbleContainer( + direction = "ltr", + body = BoxComponent( + size = "xs", + layout = "vertical", + contents = [ + TextComponent( + text = f"目前沒有{status}資料!", + size = "lg", + weight = "bold", + color = "#AAAAAA" + ) + ] + ) + ) + + record_list = FlexSendMessage( + alt_text = f"我的{status}", + contents = index + ) + + return record_list +#-------------------------------------------------------------------------------------- +def climate(activity_no, activity_name): + climate = BoxComponent( + layout = "vertical", + spacing = "sm", + background_color = "#A7D5E1", + width = "80px", + height = "25.5px", + contents = [ + TextComponent( + text = "天氣預報", + size = "sm", + color = "#FFFFFF", + align = "center", + offset_top = "2.5px" + ) + ], + action = PostbackAction( + label = "天氣預報", + data = f"climate_{activity_no}", + display_text = f"{activity_name}的天氣預報" + ) + ) + return climate + +def info_text(text, color = "#8c8c8c", size = "xs", weight = "regular"): + info_text = TextComponent( + text = text, + wrap = True, + color = color, + size = size, + weight = weight + ) + return info_text + +def button(label, data, display_text): + button = ButtonComponent( + style = "primary", + height = "sm", + color = "#A7D5E1", + gravity = "bottom", + action = PostbackAction( + label = label, + data = data, + display_text = display_text + ) + ) + return button + +def activity_info(data, owner_info = None): + activity_id, activity_name = data[0], data[2] + activity_dt, location, cost, photo = f"{data[3]} {str(data[4])[:5]}", data[5], data[9], data[12] + contents = [ + info_text(f"地點: {location}"), + info_text(f"時間: {activity_dt}"), + info_text(f"費用: {cost}") + ] + if not owner_info: + attendee, people, condition = data[13], data[8], data[14] + contents += [ + info_text(f"已報名人數: {attendee}/{people}"), + info_text(f"狀態: {condition}"), + ] + footer = BoxComponent( + layout = "horizontal", + spacing = "sm", + contents = [ + button("報名者資訊", f"報名者資訊_{activity_id}", "查看報名者資訊"), + button("結束報名", f"結束報名_{activity_id}", "結束報名") + ] + ) + else: + owner_name, owner_phone = owner_info + contents += [ + info_text(f"主揪: {owner_name}"), + info_text(f"主揪電話: {owner_phone}") + ] + footer = None + + if "https://i.imgur.com/" not in photo: + link = "https://scdn.line-apps.com/n/channel_devcenter/img/flexsnapshot/clip/clip11.jpg" + else: + link = f"{photo}" + + activity_info = BubbleContainer( + size = "kilo", + direction = "ltr", + header = BoxComponent( + layout = "vertical", + background_color = "#A7D5E1", + contents = [ + TextComponent( + text = "活動詳細資訊", + size = "lg", + align = "start", + color = "#ffffff" + ) + ] + ), + hero = ImageComponent( + size = "full", + aspectMode = "cover", + aspectRatio = "320:213", + url = f"{link}" + ), + body = BoxComponent( + layout = "vertical", + paddingAll = "13px", + spacing = "md", + contents = [ + info_text(activity_name, color = "#000000", size = "md", weight = "bold"), + climate(activity_id, activity_name), + BoxComponent( + layout = "vertical", + margin = "md", + spacing = "sm", + contents = contents + ) + ] + ), + footer = footer + ) + return activity_info + +def registration_info(user_info, registration_id, activity_id): + name, phone = user_info + + registration_info = BubbleContainer( + size = "kilo", + direction = "ltr", + header = BoxComponent( + layout = "vertical", + background_color = "#A7D5E1", + contents = [ + TextComponent( + text = "報名資訊", + size = "lg", + align = "start", + color = "#ffffff" + ) + ] + ), + body = BoxComponent( + layout = "vertical", + paddingAll = "13px", + spacing = "md", + contents = [ + BoxComponent( + layout = "vertical", + margin = "md", + spacing = "sm", + contents = [ + info_text(f"姓名: {name}", size = "md"), + info_text(f"電話: {phone}", size = "md") + ] + ) + ] + ), + footer = BoxComponent( + layout = "horizontal", + spacing = "sm", + contents = [ + button("取消報名", f"取消報名_{registration_id}_{activity_id}", "取消報名") + ] + ) + ) + return registration_info + +def activity_registration_info(activity_data, owner_info, user_info, registration_id): + activity_id = activity_data[0] + info_content = [activity_info(activity_data, owner_info), registration_info(user_info, registration_id, activity_id)] + + activity_registration_info = FlexSendMessage( + alt_text = "活動資訊與報名資訊", + contents = CarouselContainer( + contents = info_content + ) + ) + return activity_registration_info + +def send_activity_info(data): + send_activity_info = FlexSendMessage( + alt_text = "我的開團資訊", + contents = activity_info(data) + ) + + return send_activity_info +#-------------------------------------------------------------------------- +def attendee_info(activity_name, attendee_data): + print(attendee_data) + if attendee_data: + attendee_info = [f"{activity_name}", "報名者資訊:"] + [f"{data[0]} {data[1]}" for data in attendee_data] + attendee_info = TextSendMessage( + text = "\n".join(attendee_info) + ) + + else: + attendee_info = TextSendMessage( + text = "目前無人報名" + ) + + return attendee_info diff --git a/hang_out/templates/flexmsg_registration.py b/hang_out/templates/flexmsg_registration.py new file mode 100644 index 0000000..0e302d2 --- /dev/null +++ b/hang_out/templates/flexmsg_registration.py @@ -0,0 +1,403 @@ +from linebot.models import * +from templates import flexmsg_activity + +def quick_reply_button(activity_type): + quick_reply_button = QuickReplyButton( + action = PostbackAction( + label = activity_type, + data = f"報名活動類型_{activity_type}", + display_text = activity_type + ) + ) + return quick_reply_button + +def activity_type(): + all_type = ["登山踏青", "桌遊麻將", "吃吃喝喝", "唱歌跳舞"] + activity_type = TextSendMessage( + text = "請選擇報名活動類型", + quick_reply = QuickReply( + items = [quick_reply_button(activity_type) for activity_type in all_type] + ) + ) + return activity_type +#----------------------------------------------------------------- +def activity_in_index(activity_id, activity_name): + activity_in_index = BoxComponent( + layout = "baseline", + contents = [ + IconComponent( + url = "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png", + size = "sm" + ), + TextComponent( + text = f"{activity_name}", # activity_name + align = "start", + size = "md", + color = "#227C9D", + margin = "md", + action = PostbackAction( + display_text = f"我想知道 {activity_name} 的詳細資訊", + data = f"詳細資訊_{activity_id}" + ) + ) + ] + ) + return activity_in_index + +def page_button(new_page, activity_type, i): + i = i - 8 if new_page == "上一頁" else i + 8 + page_button = ButtonComponent( + action = PostbackAction( + label = new_page, + data = f"newPage_{activity_type}_{i}_", + display_text = new_page + ), + height = "sm", + style = "primary", + color = "#A7D5E1", + gravity = "bottom" + ) + return page_button + +def carousel_index(data, activity_type, i = 0): + if i < 0: + i = 0 + elif i >= len(data): + i -= 8 + + + index_content = [activity_in_index(row[0], row[2]) for row in data[i: i+8]] + + carousel_index = BubbleContainer( + size = "kilo", + direction = "ltr", + header = BoxComponent( + layout = "horizontal", + contents = [ + TextComponent( + text = f"{activity_type}-活動列表", + size = "lg", + weight = "bold", + color = "#AAAAAA" + ) + ] + ), + body = BoxComponent( + layout = "vertical", + spacing = "md", + contents = index_content + ), + footer = BoxComponent( + layout = "horizontal", + spacing = "sm", + contents = [ + page_button("上一頁", activity_type, i), + page_button("下一頁", activity_type, i) + ] + ) + ) + return carousel_index + +def climate(activity_id, activity_name): + climate = BoxComponent( + layout = "vertical", + spacing = "sm", + background_color = "#A7D5E1", + width = "80px", + height = "25.5px", + contents = [ + TextComponent( + text = "天氣預報", + size = "sm", + color = "#FFFFFF", + align = "center", + offset_top = "2.5px" + ) + ], + action = PostbackAction( + label = "天氣預報", + data = f"climate_{activity_id}", + display_text = f"{activity_name}的天氣預報" + ) + ) + return climate + +def carousel_text(text): + carousel_text = TextComponent( + text = text, + wrap = True, + color = "#8c8c8c", + size = "xs", + ) + return carousel_text + +def button(label, data, display_text): + button = ButtonComponent( + style = "link", + height = "sm", + color = "#229C8F", + gravity = "bottom", + action = PostbackAction( + label = label, + data = data, + display_text = display_text + ) + ) + return button + +def carousel(data, activity_type, i = 0): + if i < 0: + i = 0 + elif i >= len(data): + i -= 8 + + if data: + carousel = [carousel_index(data, activity_type, i)] #第一頁的列表 + + for row in data[i: i+8]: + activity_id = row[0] + activity_type = row[1] + activity_name = row[2] + activity_time = f"{row[3]} {str(row[4])[:5]}" + location = row[5] + cost = row[9] + photo = row[12] + + if "https://i.imgur.com/" not in photo: + link = "https://scdn.line-apps.com/n/channel_devcenter/img/flexsnapshot/clip/clip11.jpg" + else: + link = f"{photo}" + + main = BubbleContainer( + size = "kilo", + direction = "ltr", + hero = ImageComponent( + size = "full", + aspectMode = "cover", + aspectRatio = "320:213", + url = f"{link}" + ), + body = BoxComponent( + layout = "vertical", + paddingAll = "13px", + spacing = "md", + contents = [ + TextComponent( + text = f"{activity_name}", + weight = "bold", + size = "md", + wrap = True + ), + climate(activity_id, activity_name), + BoxComponent( + layout = "vertical", + margin = "md", + spacing = "sm", + contents = [ + carousel_text(f"地點: {location}"), + carousel_text(f"時間: {activity_time}"), + carousel_text(f"費用: {cost}") + ] + ) + ] + ), + footer = BoxComponent( + layout = "horizontal", + contents = [ + button("立即報名", f"立即報名_{activity_id}", f"我要報名 {activity_name}!"), + SeparatorComponent(), + button("詳細資訊", f"詳細資訊_{activity_id}", f"我想知道 {activity_name} 的詳細資訊!") + ] + ) + ) + carousel.append(main) + + else: + carousel = [ + BubbleContainer( + direction = "ltr", + body = BoxComponent( + size = "xs", + layout = "vertical", + contents = [ + TextComponent( + text = "目前無資料", + size = "lg", + weight = "bold", + color = "#AAAAAA" + ) + ] + ) + ) + ] + + msg_carousel = FlexSendMessage( + alt_text = "可報名活動", + contents = CarouselContainer( + contents = carousel + ) + ) + return msg_carousel +#----------------------------------------------------------------- +def more_info_header(activity_name): + more_info_header = BoxComponent( + layout = "vertical", + contents = [ + TextComponent( + text = f"{activity_name}\n 活動資訊如下:", + weight = "bold", + size = "lg", + align = "start", + color = "#000000" + ) + ] + ) + return more_info_header + +def info_text(text, info): + info_text = TextComponent( + text = f"{text}:{info}", + margin = "md", + size = "md", + wrap = True, + ) + return info_text + +def more_info_content(activity_info): + activity_dt = f"{activity_info[2]} {str(activity_info[3])[:5]}" + + more_info_content = [] + for i in [0, 1, 2, 4, 7, 8, 9, 10, 12]: + text, info = flexmsg_activity.columns_text[i], activity_info[i] + if i == 3: + info = activity_dt + more_info_content.append(info_text(text, info)) + return more_info_content + +def more_info(activity_info, activity_id): + activity_name = activity_info[1] + more_info = FlexSendMessage( + alt_text = "詳細活動資訊", + contents = BubbleContainer( + direction = "ltr", + header = more_info_header(activity_name), + body = BoxComponent( + layout = "vertical", + contents = more_info_content(activity_info) + ), + footer = BoxComponent( + layout = "vertical", + contents = [ + button("立即報名", f"立即報名_{activity_id}", f"我要報名 {activity_name}!") + ] + ) + ) + ) + return more_info +#--------------------------------------------------------------------- +def duplication(): + duplication = FlexSendMessage( + alt_text = "不可重複報名", + contents = BubbleContainer( + direction = "ltr", + body = BoxComponent( + size = "xs", + layout = "vertical", + contents = [ + TextComponent( + text = "不可重複報名! \n重新選擇想要報名的活動類型", + size = "lg", + weight = "bold", + color = "#AAAAAA", + wrap = True + ) + ] + ) + ) + ) + + duplication = [duplication, activity_type()] + return duplication +#------------------------------------------------------------------------ +def content(text, info): + content = TextComponent( + text = f"{text}:{info}", + size = "md", + flex = 5, + align = "start", + wrap = True, + ) + return content + +def edit(text, col): + edit = TextComponent( + text = "修改", + size = "md", + flex = 1, + align = "end", + gravity = "top", + color = "#229C8F", + action = PostbackAction( + label = f"修改{text}", + data = f"修改_users_{col}", + display_text = f"修改{text}" + ) + ) + return edit + +def content_edit(info, text, col): + content_edit = BoxComponent( + layout = "horizontal", + margin = "lg", + contents = [ + content(text, info), + SeparatorComponent(), + edit(text, col) + ] + ) + return content_edit + +def summary(data, activity_name): + activity_id = data[2] + registration_id = data[3] + name = data[0] + phone = data[1] + + summary = FlexSendMessage( + alt_text = "請確認報名資訊", + contents = BubbleContainer( + direction = "ltr", + header = BoxComponent( + layout = "vertical", + contents = [ + TextComponent( + text = "請確認報名資訊:", + weight = "bold", + size = "lg", + align = "start", + color = "#000000" + ) + ] + ), + body = BoxComponent( + layout = "vertical", + contents = [ + content("活動名稱", activity_name), + SeparatorComponent( + margin = "lg" + ), + content_edit(name, "姓名", "name"), + content_edit(phone, "電話", "phone") + ] + ), + footer = BoxComponent( + layout = "horizontal", + contents = [ + button("確認報名", f"確認報名_{activity_id}", "確認報名"), + SeparatorComponent(), + button("取消報名", f"取消報名_{registration_id}_{activity_id}", "取消報名") + ] + ) + ) + ) + return summary