Spaces:
Running
Running
from fastapi import FastAPI, Request, Header, BackgroundTasks, HTTPException, status | |
from linebot import LineBotApi, WebhookHandler | |
from linebot.exceptions import InvalidSignatureError | |
from linebot.models import MessageEvent, TextMessage, TextSendMessage, ImageSendMessage, ImageMessage, AudioMessage, AudioSendMessage | |
import os | |
import traceback | |
class LineBotApp: | |
""" | |
Line Bot 應用程式類別,負責處理 Line 平台的互動,包括 webhook 接收、訊息處理和狀態管理。 | |
""" | |
def __init__(self, channel_access_token, channel_secret, chatbot): | |
""" | |
初始化 LineBotApp 實例。 | |
""" | |
self.line_bot_api = LineBotApi(channel_access_token) | |
self.line_handler = WebhookHandler(channel_secret) | |
self.chatbot = chatbot | |
self.working_status = os.getenv("DEFALUT_TALKING", default="false").lower() == "true" | |
print(f"Default working status: {self.working_status}") | |
# 啟動/停用關鍵字保持不變 | |
self.activation_word = "嘿橘橘" | |
self.deactivation_word = "退下橘橘" | |
self.bot_activated_states = {} | |
self._setup_message_handler() | |
def _setup_message_handler(self): | |
""" | |
設定 Line Message Handler。 | |
""" | |
def handle_message_event(event): | |
# 將事件傳遞給 handle_message 處理 | |
self.handle_message(event) | |
def handle_webhook(self, body, signature): | |
""" | |
處理 Line Webhook 請求。 | |
""" | |
try: | |
self.line_handler.handle(body, signature) | |
return "ok" | |
except InvalidSignatureError: | |
print("Invalid signature error.") | |
raise HTTPException(status_code=400, detail="Invalid signature") | |
except Exception as e: | |
print(f"Error handling webhook: {e}") | |
traceback.print_exc() | |
return "ok" | |
def handle_message(self, event): | |
""" | |
處理 Line 訊息事件,包含啟動/停用邏輯。 | |
""" | |
user_id = event.source.user_id # 發送訊息的使用者 ID | |
source_id = self._get_source_id(event) # 訊息來源 ID (user/group/room) | |
print(f"Received message event from source_id: {source_id}, user_id: {user_id}, type: {event.message.type}") | |
# 優先處理文字訊息中的控制指令 | |
if isinstance(event.message, TextMessage): | |
message_text = event.message.text.strip() | |
# 檢查是否為啟動指令 | |
if message_text == self.activation_word: | |
print(f"Activation command received for {source_id}") | |
self.bot_activated_states[source_id] = True | |
try: | |
self.line_bot_api.reply_message( | |
event.reply_token, | |
TextSendMessage(text="""喵!橘橘來囉~有什麼可以協助你的嗎?文生圖=["生成一張","畫一張"],編輯圖片=["編輯圖片","幫我改圖","編輯這張圖"]增強檢索=["開啟查詢模式","關閉查詢模式"]""") | |
) | |
except Exception as reply_err: | |
print(f"Error replying to activation command: {reply_err}") | |
return # 處理完啟動指令後結束 | |
# 檢查是否為停用指令 | |
elif message_text == self.deactivation_word: | |
print(f"Deactivation command received for {source_id}") | |
self.bot_activated_states[source_id] = False | |
try: | |
self.line_bot_api.reply_message( | |
event.reply_token, | |
TextSendMessage(text="喵~橘橘先去睡覺了,需要我時再叫我~") | |
) | |
except Exception as reply_err: | |
print(f"Error replying to deactivation command: {reply_err}") | |
return # 處理完停用指令後結束 | |
# --- 如果不是控制指令,檢查是否已啟動 --- | |
# 從狀態字典獲取狀態,如果字典中沒有,則使用預設的 self.working_status (現在是 False) | |
is_activated = self.bot_activated_states.get(source_id, self.working_status) | |
print(f"Bot activated state for {source_id}: {is_activated}") | |
# 如果未啟動,則忽略所有非控制指令的訊息 | |
if not is_activated: | |
print(f"Bot not activated for {source_id}. Ignoring message.") | |
return # 直接結束,不處理 | |
# --- 如果已啟動,則根據訊息類型轉發給 ChatBot --- | |
print(f"Bot is activated for {source_id}. Processing message...") | |
try: | |
if isinstance(event.message, TextMessage): | |
# 處理文字訊息 (已排除控制指令) | |
self.chatbot.handle_text_message(event, self.line_bot_api) | |
elif isinstance(event.message, ImageMessage): | |
# 處理圖片訊息 | |
self.chatbot.handle_image_message(event, self.line_bot_api, self) # 傳遞 self (LineBotApp 實例) | |
elif isinstance(event.message, AudioMessage): | |
# 處理語音訊息 | |
self.chatbot.handle_audio_message(event, self.line_bot_api, self) # 傳遞 self (LineBotApp 實例) | |
# 可以根據需要添加對其他訊息類型 (如貼圖、位置等) 的處理 | |
else: | |
print(f"Ignoring unsupported message type: {event.message.type}") | |
except Exception as e: | |
# 捕捉 ChatBot 處理過程中可能發生的錯誤 | |
print(f"Error during chatbot message handling: {e}") | |
traceback.print_exc() | |
# 可以選擇是否回覆通用錯誤訊息給使用者 | |
try: | |
self.line_bot_api.reply_message( | |
event.reply_token, | |
TextSendMessage(text="喵~糟糕,橘橘好像有點問題,請稍後再試一次!") | |
) | |
except Exception as reply_error: | |
print(f"Failed to send error reply after chatbot error: {reply_error}") | |
def _get_source_id(self, event): | |
""" | |
根據訊息來源取得唯一識別 ID。 | |
""" | |
source_type = event.source.type | |
if source_type == 'user': return f"user_{event.source.user_id}" | |
elif source_type == 'group': return f"group_{event.source.group_id}" | |
elif source_type == 'room': return f"room_{event.source.room_id}" | |
user_id = getattr(event.source, 'user_id', None) | |
return f"unknown_{user_id}" if user_id else "unknown_source" | |
def get_image_url(self, message_id): | |
try: | |
message_content = self.line_bot_api.get_message_content(message_id) | |
tmp_dir = "/tmp/images"; os.makedirs(tmp_dir, exist_ok=True) | |
file_path = os.path.join(tmp_dir, f"{message_id}.jpg") | |
print(f"Downloading image {message_id} to {file_path}") | |
with open(file_path, 'wb') as fd: | |
for chunk in message_content.iter_content(): fd.write(chunk) | |
print(f"Image {message_id} downloaded successfully.") | |
return file_path | |
except Exception as e: | |
print(f"獲取圖片時出錯 (message_id: {message_id}): {e}"); traceback.print_exc(); return None | |
def get_audio_url(self, message_id): | |
try: | |
message_content = self.line_bot_api.get_message_content(message_id) | |
tmp_dir = "/tmp/audio"; os.makedirs(tmp_dir, exist_ok=True) | |
file_path = os.path.join(tmp_dir, f"{message_id}.m4a") | |
print(f"Downloading audio {message_id} to {file_path}") | |
with open(file_path, 'wb') as fd: | |
for chunk in message_content.iter_content(): fd.write(chunk) | |
print(f"Audio {message_id} downloaded successfully.") | |
return file_path | |
except Exception as e: | |
print(f"獲取語音檔案時出錯 (message_id: {message_id}): {e}"); traceback.print_exc(); return None |