cwadayi commited on
Commit
d422181
·
verified ·
1 Parent(s): 7ac3bdd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +110 -129
app.py CHANGED
@@ -1,201 +1,182 @@
1
  import os
2
- from flask import Flask, request, abort
 
3
  from linebot.v3 import WebhookHandler
4
  from linebot.v3.exceptions import InvalidSignatureError
5
  from linebot.v3.messaging import (
6
  Configuration, ApiClient, MessagingApi,
7
- ReplyMessageRequest, TextMessage
8
  )
9
  from linebot.v3.webhooks import MessageEvent, TextMessageContent
10
 
11
  import requests
12
  import pandas as pd
 
13
  from datetime import datetime, timedelta
14
 
15
  # --- 環境變數 ---
16
  CHANNEL_ACCESS_TOKEN = os.getenv('CHANNEL_ACCESS_TOKEN')
17
  CHANNEL_SECRET = os.getenv('CHANNEL_SECRET')
 
 
18
 
19
  # --- Flask & LINE Bot 初始化 ---
20
  app = Flask(__name__)
21
 
 
 
 
 
22
  configuration = Configuration(access_token=CHANNEL_ACCESS_TOKEN)
23
  handler = WebhookHandler(CHANNEL_SECRET)
24
 
25
-
26
- # --- 歡迎頁面路由 ---
27
  @app.route("/", methods=['GET'])
28
  def home():
29
- return """
30
- <html>
31
- <head>
32
- <title>LINE Bot Server</title>
33
- <style>
34
- body { font-family: Arial, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f0f2f5; margin: 0; }
35
- .container { text-align: center; padding: 40px; background-color: white; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
36
- h1 { color: #1dcd00; }
37
- p { color: #333; font-size: 1.2em; }
38
- .status { font-weight: bold; color: #28a745; }
39
- </style>
40
- </head>
41
- <body>
42
- <div class="container">
43
- <h1>✓ LINE Bot Server is Running</h1>
44
- <p>This is the backend service for the Earthquake Alert Bot.</p>
45
- <p>The service is <span class="status">active</span> and listening for webhook events from LINE.</p>
46
- </div>
47
- </body>
48
- </html>
49
- """
50
 
51
  # --- 地震查詢核心邏輯 ---
52
  USGS_API_BASE_URL = "https://earthquake.usgs.gov/fdsnws/event/1/query"
53
 
54
  def fetch_earthquake_data_for_line():
55
- """查詢過去 24 小時內全球規模 5.0 以上的地震。"""
 
56
  now = datetime.now()
57
  yesterday = now - timedelta(days=1)
58
-
59
- params = {
60
- "format": "geojson",
61
- "starttime": yesterday.strftime('%Y-%m-%d'),
62
- "endtime": now.strftime('%Y-%m-%d'),
63
- "minmagnitude": 5.0,
64
- "limit": 10,
65
- "orderby": "time"
66
- }
67
-
68
  try:
69
  response = requests.get(USGS_API_BASE_URL, params=params, timeout=15)
70
  response.raise_for_status()
71
  data = response.json()
72
-
73
  features = data.get('features', [])
74
- if not features:
75
- return "✅ 過去24小時內,全球無規模 5.0 以上的顯著地震。"
76
-
77
  reply_text = f"🚨 近 24 小時全球顯著地震 (M≥5.0):\n{'-'*20}\n"
78
-
79
  for feature in features:
80
  prop = feature['properties']
81
- mag = prop['mag']
82
- place = prop['place']
83
- event_time = datetime.fromtimestamp(prop['time'] / 1000).strftime('%H:%M')
84
-
85
- reply_text += f"震級: {mag:.1f} | 時間: {event_time} (UTC)\n地點: {place}\n\n"
86
-
87
  return reply_text.strip()
 
88
 
89
- except requests.exceptions.RequestException as e:
90
- return f" 查詢失敗,無法連接到 USGS 伺服器: {e}"
91
- except Exception as e:
92
- return f"❌ 處理資料時發生未知錯誤: {e}"
93
-
94
-
95
- # --- 「臺灣地震」查詢函式 ---
96
- def fetch_taiwan_earthquake_data():
97
- """查詢今年以來,台灣地區規模 5.0 以上的地震。"""
98
  now = datetime.now()
99
  start_of_year = now.replace(month=1, day=1).strftime('%Y-%m-%d')
100
  today_str = now.strftime('%Y-%m-%d')
101
-
102
  params = {
103
- "format": "geojson",
104
- "starttime": start_of_year,
105
- "endtime": today_str,
106
- "minmagnitude": 5.0,
107
- "minlatitude": 21,
108
- "maxlatitude": 26,
109
- "minlongitude": 119,
110
- "maxlongitude": 123,
111
- "limit": 100,
112
- "orderby": "time"
113
  }
114
-
115
  try:
116
  response = requests.get(USGS_API_BASE_URL, params=params, timeout=20)
117
  response.raise_for_status()
118
  data = response.json()
119
-
120
  features = data.get('features', [])
121
- count = len(features)
122
 
123
- if not features:
124
- return f"✅ 今年 ({now.year}年) 以來,在您指定的範圍內尚無規模 5.0 以上的顯著地震紀錄。"
125
-
126
- reply_text = f"🇹🇼 今年 ({now.year}年) 台灣區域顯著地震 (M≥5.0),共 {count} 筆:\n{'-'*20}\n"
127
-
128
- for feature in features:
129
- prop = feature['properties']
130
- mag = prop['mag']
131
- place = prop['place']
132
- event_time_utc = datetime.fromtimestamp(prop['time'] / 1000)
133
- time_str = event_time_utc.strftime('%Y-%m-%d %H:%M')
134
-
135
- reply_text += f"震級: {mag:.1f} | 時間: {time_str} (UTC)\n地點: {place}\n\n"
136
-
137
- if len(reply_text) > 4800:
138
- reply_text = reply_text[:4800] + "\n... (資料過多,僅顯示部分)"
139
-
140
- return reply_text.strip()
141
-
142
- except requests.exceptions.RequestException as e:
143
- return f" 查詢失敗,無法連接到 USGS 伺服器: {e}"
144
- except Exception as e:
145
- return f"❌ 處理資料時發生未知錯誤: {e}"
146
-
 
 
 
 
 
 
 
 
147
 
148
  # --- Flask Webhook 路由 ---
149
  @app.route("/callback", methods=['POST'])
150
  def callback():
151
  signature = request.headers['X-Line-Signature']
152
  body = request.get_data(as_text=True)
153
- app.logger.info("Request body: " + body)
154
-
155
  try:
156
  handler.handle(body, signature)
157
  except InvalidSignatureError:
158
- app.logger.info("Invalid signature. Please check your channel secret.")
159
  abort(400)
160
  return 'OK'
161
 
162
-
163
  # --- LINE 訊息處理 (有修改) ---
164
  @handler.add(MessageEvent, message=TextMessageContent)
165
  def handle_message(event):
166
- user_message = event.message.text.strip().lower() # 統一轉為小寫以便比對
167
- reply_text = ""
168
-
169
- # --- ✨✨✨ 修改點:新增 /help 指令 ✨✨✨ ---
170
- if user_message == "/help":
171
- reply_text = """📖 地震預警dayichen 指令說明
172
-
173
- 您可以傳送以下指令來與我互動:
174
-
175
- ➡️ /help
176
- 說明:顯示此幫助訊息,列出所有可用指令。
177
-
178
- ➡️ 地震
179
- 說明:查詢全球最近 24 小時內,芮氏規模 5.0 以上的顯著地震。
180
-
181
- ➡️ 臺灣地震 (或 台灣地震)
182
- 說明:查詢今年以來,在台灣區域 (緯度 21-26°, 經度 119-123°) 發生的芮氏規模 5.0 以上地震。
183
-
184
- ➡️ 你好
185
- 說明:顯示歡迎訊息。"""
186
- elif "臺灣地震" in user_message or "台灣地震" in user_message:
187
- reply_text = fetch_taiwan_earthquake_data()
188
- elif "地震" in user_message or "quake" in user_message:
189
- reply_text = fetch_earthquake_data_for_line()
190
- elif "你好" in user_message or "hi" in user_message:
191
- reply_text = "👋 你好!我是地震查詢機器人。\n\n試著傳送「地震」或「臺灣地震」,或輸入 /help 查看所有指令。"
192
- else:
193
- # 如果不是已知的指令,可以選擇不回覆,或回覆提示訊息
194
- # reply_text = f"無法識別指令「{event.message.text}」。\n請輸入 /help 查看所有可用指令。"
195
- return
196
-
197
  with ApiClient(configuration) as api_client:
198
  line_bot_api = MessagingApi(api_client)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  line_bot_api.reply_message_with_http_info(
200
  ReplyMessageRequest(
201
  reply_token=event.reply_token,
 
1
  import os
2
+ import uuid
3
+ from flask import Flask, request, abort, send_from_directory
4
  from linebot.v3 import WebhookHandler
5
  from linebot.v3.exceptions import InvalidSignatureError
6
  from linebot.v3.messaging import (
7
  Configuration, ApiClient, MessagingApi,
8
+ ReplyMessageRequest, TextMessage, ImageSendMessage
9
  )
10
  from linebot.v3.webhooks import MessageEvent, TextMessageContent
11
 
12
  import requests
13
  import pandas as pd
14
+ import plotly.express as px
15
  from datetime import datetime, timedelta
16
 
17
  # --- 環境變數 ---
18
  CHANNEL_ACCESS_TOKEN = os.getenv('CHANNEL_ACCESS_TOKEN')
19
  CHANNEL_SECRET = os.getenv('CHANNEL_SECRET')
20
+ # 從 Secrets 讀取我們設定的 Space 公開網址
21
+ HF_SPACE_URL = os.getenv('HF_SPACE_URL')
22
 
23
  # --- Flask & LINE Bot 初始化 ---
24
  app = Flask(__name__)
25
 
26
+ # 建立一個資料夾來存放靜態圖片檔
27
+ if not os.path.exists('static'):
28
+ os.makedirs('static')
29
+
30
  configuration = Configuration(access_token=CHANNEL_ACCESS_TOKEN)
31
  handler = WebhookHandler(CHANNEL_SECRET)
32
 
33
+ # --- 歡迎頁面 & 靜態圖片服務路由 ---
 
34
  @app.route("/", methods=['GET'])
35
  def home():
36
+ # ... (歡迎頁面 HTML,與之前相同) ...
37
+ return "<h1>✓ LINE Bot Server is Running</h1>"
38
+
39
+ @app.route('/static/<path:filename>')
40
+ def serve_static(filename):
41
+ """這個路由讓外部可以存取 static 資料夾內的檔案"""
42
+ return send_from_directory('static', filename)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
  # --- 地震查詢核心邏輯 ---
45
  USGS_API_BASE_URL = "https://earthquake.usgs.gov/fdsnws/event/1/query"
46
 
47
  def fetch_earthquake_data_for_line():
48
+ """查詢全球地震並回傳格式化的文字"""
49
+ # ... (此函式與之前完全相同) ...
50
  now = datetime.now()
51
  yesterday = now - timedelta(days=1)
52
+ params = {"format": "geojson","starttime": yesterday.strftime('%Y-%m-%d'),"endtime": now.strftime('%Y-%m-%d'),"minmagnitude": 5.0,"limit": 10,"orderby": "time"}
 
 
 
 
 
 
 
 
 
53
  try:
54
  response = requests.get(USGS_API_BASE_URL, params=params, timeout=15)
55
  response.raise_for_status()
56
  data = response.json()
 
57
  features = data.get('features', [])
58
+ if not features: return "✅ 過去24小時內,全球無規模 5.0 以上的顯著地震。"
 
 
59
  reply_text = f"🚨 近 24 小時全球顯著地震 (M≥5.0):\n{'-'*20}\n"
 
60
  for feature in features:
61
  prop = feature['properties']
62
+ reply_text += f"震級: {prop['mag']:.1f} | 時間: {datetime.fromtimestamp(prop['time']/1000).strftime('%H:%M')} (UTC)\n地點: {prop['place']}\n\n"
 
 
 
 
 
63
  return reply_text.strip()
64
+ except Exception as e: return f"❌ 查詢失敗: {e}"
65
 
66
+ def fetch_taiwan_earthquake_data_df():
67
+ """查詢今年台灣地震並回傳 DataFrame 或錯誤訊息(str)"""
 
 
 
 
 
 
 
68
  now = datetime.now()
69
  start_of_year = now.replace(month=1, day=1).strftime('%Y-%m-%d')
70
  today_str = now.strftime('%Y-%m-%d')
 
71
  params = {
72
+ "format": "geojson", "starttime": start_of_year, "endtime": today_str,
73
+ "minmagnitude": 5.0, "minlatitude": 21, "maxlatitude": 26,
74
+ "minlongitude": 119, "maxlongitude": 123, "limit": 250, "orderby": "time"
 
 
 
 
 
 
 
75
  }
 
76
  try:
77
  response = requests.get(USGS_API_BASE_URL, params=params, timeout=20)
78
  response.raise_for_status()
79
  data = response.json()
 
80
  features = data.get('features', [])
81
+ if not features: return f"✅ 今年 ({now.year}年) 以來,在指定範圍內尚無規模 5.0 以上的顯著地震紀錄。"
82
 
83
+ earthquake_list = [{'latitude': f['geometry']['coordinates'][1],'longitude': f['geometry']['coordinates'][0],'magnitude': f['properties']['mag'],'place': f['properties']['place'],'time': datetime.fromtimestamp(f['properties']['time']/1000)} for f in features]
84
+ return pd.DataFrame(earthquake_list)
85
+ except Exception as e: return f"❌ 查詢失敗: {e}"
86
+
87
+ # --- ✨✨✨ 新增的繪圖函式 ✨✨✨ ---
88
+ def create_and_save_map(df):
89
+ """接收 DataFrame,繪製地圖,儲存圖片,並回傳檔名"""
90
+ fig = px.scatter_mapbox(
91
+ df,
92
+ lat="latitude", lon="longitude",
93
+ size="magnitude", color="magnitude",
94
+ hover_name="place",
95
+ hover_data={'time': '|%Y-%m-%d %H:%M', 'magnitude': ':.1f'},
96
+ color_continuous_scale=px.colors.sequential.YlOrRd,
97
+ size_max=30,
98
+ mapbox_style="carto-darkmatter",
99
+ center={"lat": 23.5, "lon": 121.0}, # 地圖中心點設在台灣
100
+ zoom=6
101
+ )
102
+ fig.update_layout(
103
+ title=f"<b>今年 ({datetime.now().year}) 台灣區域顯著地震 (M≥5.0)</b>",
104
+ margin={"r": 0, "t": 40, "l": 0, "b": 0}
105
+ )
106
+
107
+ # 產生一個獨一無二的檔名,避免檔案衝突
108
+ filename = f"map_{uuid.uuid4().hex}.png"
109
+ filepath = os.path.join('static', filename)
110
+
111
+ # 儲存圖片,kaleido 引擎會在這裡作用
112
+ fig.write_image(filepath, scale=2) # scale=2 讓圖片更清晰
113
+
114
+ return filename
115
 
116
  # --- Flask Webhook 路由 ---
117
  @app.route("/callback", methods=['POST'])
118
  def callback():
119
  signature = request.headers['X-Line-Signature']
120
  body = request.get_data(as_text=True)
 
 
121
  try:
122
  handler.handle(body, signature)
123
  except InvalidSignatureError:
 
124
  abort(400)
125
  return 'OK'
126
 
 
127
  # --- LINE 訊息處理 (有修改) ---
128
  @handler.add(MessageEvent, message=TextMessageContent)
129
  def handle_message(event):
130
+ user_message = event.message.text.strip().lower()
131
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  with ApiClient(configuration) as api_client:
133
  line_bot_api = MessagingApi(api_client)
134
+
135
+ # --- ✨✨✨ 修改點:加入對「臺灣地震畫圖」的處理 ✨✨✨ ---
136
+ if "臺灣地震畫圖" in user_message or "台灣地震畫圖" in user_message:
137
+ # 1. 顯示「處理中」的提示,提升使用者體驗
138
+ line_bot_api.reply_message(ReplyMessageRequest(
139
+ reply_token=event.reply_token, messages=[TextMessage(text="🗺️ 收到指令!正在繪製地震分佈圖,請稍候...")]
140
+ ))
141
+
142
+ # 2. 獲取地震資料
143
+ result = fetch_taiwan_earthquake_data_df()
144
+
145
+ # 3. 判斷結果是 DataFrame 還是錯誤訊息
146
+ if isinstance(result, pd.DataFrame):
147
+ # 4. 如果是 DataFrame,繪製地圖並取得檔名
148
+ filename = create_and_save_map(result)
149
+ # 5. 組合圖片的公開 URL
150
+ image_url = f"{HF_SPACE_URL}/static/{filename}"
151
+
152
+ # 6. 這次不使用 reply_message,而是用 push_message 將圖片傳送給使用者
153
+ line_bot_api.push_message(body={'to': event.source.user_id, 'messages': [
154
+ ImageSendMessage(original_content_url=image_url, preview_image_url=image_url)
155
+ ]})
156
+ else:
157
+ # 如果是錯誤訊息,一樣用 push_message 回傳
158
+ line_bot_api.push_message(body={'to': event.source.user_id, 'messages': [TextMessage(text=result)]})
159
+ return
160
+
161
+ # --- 其他指令的處理邏輯 (與之前相同) ---
162
+ reply_text = ""
163
+ if user_message == "/help":
164
+ reply_text = "📖 ... (幫助訊息) ..." # 為節省空間省略
165
+ elif "臺灣地震" in user_message or "台灣地震" in user_message:
166
+ # 為了避免與畫圖指令混淆,這裡可以只回傳文字
167
+ result = fetch_taiwan_earthquake_data_df()
168
+ if isinstance(result, pd.DataFrame):
169
+ # ... (格式化 DataFrame 為文字) ...
170
+ reply_text = "..."
171
+ else:
172
+ reply_text = result
173
+ elif "地震" in user_message or "quake" in user_message:
174
+ reply_text = fetch_earthquake_data_for_line()
175
+ elif "你好" in user_message or "hi" in user_message:
176
+ reply_text = "👋 ... (歡迎訊息) ..." # 為節省空間省略
177
+ else:
178
+ return
179
+
180
  line_bot_api.reply_message_with_http_info(
181
  ReplyMessageRequest(
182
  reply_token=event.reply_token,