File size: 8,115 Bytes
c5b9686
943860b
 
e1abaf3
943860b
 
dee9359
943860b
 
 
 
 
 
 
 
 
 
 
e42c87a
 
 
943860b
 
 
 
 
 
 
e42c87a
943860b
 
 
e42c87a
 
943860b
 
 
 
 
 
 
 
 
e42c87a
 
 
dee9359
 
 
 
943860b
 
e42c87a
943860b
e42c87a
 
 
943860b
e42c87a
943860b
 
dee9359
943860b
 
dee9359
e42c87a
 
 
 
7243eb3
50d24f5
e42c87a
 
 
dee9359
943860b
 
dee9359
e42c87a
 
 
 
 
 
 
 
 
 
 
 
 
 
dee9359
e42c87a
 
 
 
dee9359
e42c87a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dee9359
e42c87a
 
 
 
 
dee9359
e42c87a
 
 
 
 
 
943860b
 
 
e42c87a
943860b
 
e42c87a
 
 
 
 
943860b
 
 
 
e42c87a
dee9359
e42c87a
943860b
e42c87a
dee9359
943860b
 
e42c87a
943860b
 
 
 
e42c87a
dee9359
e42c87a
943860b
e42c87a
dee9359
943860b
 
e42c87a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
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。
        """
        @self.line_handler.add(MessageEvent, message=(ImageMessage, TextMessage, AudioMessage))
        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