File size: 14,352 Bytes
dbb6988
 
 
5a3113b
4e2f406
dbb6988
 
 
 
5a3113b
4e2f406
 
dbb6988
092c06d
dbb6988
fd5dbb4
dbb6988
fd5dbb4
 
 
 
 
 
 
dbb6988
 
 
 
 
 
 
96f79c9
 
 
 
fd5dbb4
 
 
 
 
 
96f79c9
 
 
 
 
 
 
 
 
 
 
dbb6988
3c6d6b3
fd5dbb4
 
 
dbb6988
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fd5dbb4
dbb6988
fd5dbb4
dbb6988
 
298cf0a
 
fd5dbb4
 
 
 
298cf0a
 
fd5dbb4
 
 
298cf0a
fd5dbb4
298cf0a
fd5dbb4
298cf0a
 
 
 
 
 
 
fd5dbb4
298cf0a
 
 
 
 
 
 
fd5dbb4
298cf0a
 
 
 
4e2f406
fd5dbb4
4e2f406
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fd5dbb4
4e2f406
fd5dbb4
8afb633
 
fd5dbb4
 
 
8afb633
4e2f406
 
8afb633
fd5dbb4
 
 
4e2f406
8afb633
 
 
 
 
 
 
 
 
 
 
 
 
 
4e2f406
8afb633
fd5dbb4
 
 
4e2f406
 
fd5dbb4
 
 
5a3113b
ebb1778
5a3113b
4e2f406
 
fd5dbb4
4e2f406
 
 
fd5dbb4
4e2f406
 
 
 
 
fd5dbb4
4e2f406
fd5dbb4
4e2f406
 
 
fd5dbb4
 
 
4e2f406
fd5dbb4
 
 
 
4e2f406
 
 
fd5dbb4
 
 
4e2f406
fd5dbb4
 
 
4e2f406
 
5a3113b
87eddd1
 
 
 
4e2f406
 
fd5dbb4
4e2f406
 
 
 
 
 
fd5dbb4
 
 
4e2f406
fd5dbb4
 
 
4e2f406
 
 
fd5dbb4
 
 
4e2f406
fd5dbb4
 
 
4e2f406
 
87eddd1
3c6d6b3
fd5dbb4
 
 
 
 
 
96f79c9
 
092c06d
 
fd5dbb4
 
 
092c06d
 
96f79c9
fd5dbb4
 
 
 
 
 
 
 
 
 
092c06d
298cf0a
fd5dbb4
 
298cf0a
 
 
fd5dbb4
092c06d
 
 
fd5dbb4
 
 
 
092c06d
 
 
 
fd5dbb4
 
 
 
 
092c06d
 
 
fd5dbb4
 
 
092c06d
fd5dbb4
298cf0a
dbb6988
87eddd1
fd5dbb4
 
 
87eddd1
 
 
 
 
 
fd5dbb4
 
 
 
87eddd1
 
fd5dbb4
87eddd1
 
 
3c6d6b3
dbb6988
 
 
 
 
 
 
 
 
fd5dbb4
dbb6988
 
 
fd5dbb4
dbb6988
 
 
 
 
fd5dbb4
dbb6988
 
 
 
 
 
 
 
 
 
 
5b5a95d
fd5dbb4
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
import hmac
import hashlib
import json
import asyncio
import time
from typing import Any, Dict, Optional
import httpx
from fastapi import HTTPException, Request
from loguru import logger
import facebook
import requests
from .config import Settings, get_settings

from .utils import timing_decorator_async, timing_decorator_sync, _safe_truncate


class FacebookClient:
    def __init__(
        self,
        app_secret: str,
        page_id: Optional[str] = None,
        page_token: Optional[str] = None,
        sender_id: Optional[str] = None,
    ):
        """
        Khởi tạo FacebookClient với app_secret.
        Input: app_secret (str) - Facebook App Secret.
        Output: FacebookClient instance.
        """
        self.app_secret = app_secret
        self._client = httpx.AsyncClient()
        self.page_id = page_id
        self.page_token = page_token
        self.sender_id = sender_id

    def update_context(
        self,
        page_id: Optional[str] = None,
        page_token: Optional[str] = None,
        sender_id: Optional[str] = None,
    ):
        """
        Cập nhật các thông tin context (page_id, page_token, sender_id) của client.
        Input: page_id (str), page_token (str), sender_id (str)
        Output: None
        """
        if page_id is not None:
            self.page_id = page_id
        if page_token is not None:
            self.page_token = page_token
        if sender_id is not None:
            self.sender_id = sender_id

    @timing_decorator_async
    async def verify_webhook(
        self, token: str, challenge: str, verify_token: str
    ) -> int:
        """
        Xác thực webhook Facebook bằng verify_token và trả về challenge.
        Input: token (str), challenge (str), verify_token (str)
        Output: int (challenge nếu thành công, lỗi nếu thất bại)
        """
        if token != verify_token:
            raise HTTPException(status_code=403, detail="Invalid verify token")
        return int(challenge)

    def verify_signature(self, request: Request, payload: bytes) -> bool:
        """
        Kiểm tra chữ ký X-Hub-Signature-256 để xác thực request từ Facebook.
        Input: request (Request), payload (bytes)
        Output: bool (True nếu hợp lệ, False nếu không)
        """
        signature = request.headers.get("X-Hub-Signature-256", "")
        if not signature.startswith("sha256="):
            return False

        expected = hmac.new(
            self.app_secret.encode(), payload, hashlib.sha256
        ).hexdigest()

        return hmac.compare_digest(signature[7:], expected)

    def format_message(self, text: str) -> str:
        # 1. Thay bullet markdown bằng ký hiệu khác
        text = text.replace("\n*   ", "\n- ")
        text = text.replace("\n    *   ", "\n    + ")
        text = text.replace("\n* ", "\n- ")
        text = text.replace("\n    * ", "\n    + ")
        # 2. Chuyển **text** hoặc __text__ thành *text*
        import re

        text = re.sub(r"\*\*([^\*]+)\*\*", r"*\1*", text)
        text = re.sub(r"__([^_]+)__", r"*\1*", text)
        # 3. Loại bỏ các tiêu đề markdown kiểu #, ##, ###, ...
        text = re.sub(r"^#+\s+", "", text, flags=re.MULTILINE)
        # 4. Rút gọn nhiều dòng trống liên tiếp thành 1 dòng trống
        text = re.sub(r"\n{3,}", "\n\n", text)
        # 5. Loại bỏ các markdown không hỗ trợ khác nếu cần
        return text

    def split_message(self, text: str, max_length: int = 2000) -> list:
        """
        Chia message thành các đoạn <= max_length ký tự, ưu tiên chia theo dòng.
        """
        lines = text.split("\n")
        messages = []
        current = ""
        for line in lines:
            # +1 cho ký tự xuống dòng
            if len(current) + len(line) + 1 > max_length:
                messages.append(current.rstrip())
                current = ""
            current += line + "\n"
        if current.strip():
            messages.append(current.rstrip())
        return messages

    def send_message_forwarder(
        self, access_token: str, recipient_id: str, message: str
    ) -> dict:
        """
        Gửi tin nhắn đến Facebook Messenger qua API được triển khai.

        Parameters:
            api_base_url (str): Base URL của API, ví dụ "https://your-project.vercel.app"
            recipient_id (str): PSID của người nhận
            access_token (str): Facebook Page Access Token
            message (str): Nội dung tin nhắn

        Returns:
            dict: Kết quả trả về từ API (JSON)
        """

        api_base_url = get_settings().facebook_api_base_url
        url = f"{api_base_url.rstrip('/')}/api/send-message"
        payload = {
            "recipient_id": recipient_id,
            "access_token": access_token,
            "message": message,
        }

        # Ghi lại toàn bộ payload để gỡ lỗi.
        # CẢNH BÁO: Việc này sẽ ghi lại cả PAGE_ACCESS_TOKEN. Chỉ nên dùng trong môi trường dev hoặc khi cần gỡ lỗi.
        logger.debug(
            f"[FACEBOOK_FORWARDER] Forwarding message to {url}. Full payload: {json.dumps(payload, ensure_ascii=False)}"
        )

        try:
            response = requests.post(url, json=payload, timeout=10)
            response.raise_for_status()  # Sẽ raise HTTPError cho các status 4xx/5xx
            logger.info(
                f"[FACEBOOK_FORWARDER] Forwarder API returned status {response.status_code}."
            )
            return response.json()
        except requests.HTTPError as e:
            # Lỗi HTTP (4xx, 5xx), log chi tiết hơn để gỡ lỗi phía forwarder
            error_content = "No response body"
            try:
                # Cố gắng lấy nội dung lỗi từ server để biết nguyên nhân
                error_content = e.response.text
            except Exception:
                pass
            logger.error(
                f"[FACEBOOK_FORWARDER] HTTP Error calling forwarder API: {e}. "
                f"Status: {e.response.status_code}. Payload sent: {json.dumps(payload, ensure_ascii=False)}. "
                f"Response: {error_content}"
            )
            return {"error": str(e), "details": error_content}
        except requests.RequestException as e:
            # Các lỗi request khác (timeout, connection error)
            logger.error(
                f"[FACEBOOK_FORWARDER] Request Error calling forwarder API: {e}"
            )
            return {"error": str(e)}

    def _send_message_sync(
        self, page_access_token: str, recipient_id: str, message: str
    ) -> dict:
        """
        Gửi tin nhắn sử dụng facebook-sdk với request method trực tiếp.
        """
        max_retries = 3
        retry_delay = 1  # giây

        for attempt in range(max_retries):
            try:
                graph = facebook.GraphAPI(access_token=page_access_token, version="3.1")

                # Sử dụng request method trực tiếp cho Messenger API với timeout
                result = graph.request(
                    path="me/messages",
                    post_args={
                        "recipient": {"id": recipient_id},
                        "message": {"text": message},
                    },
                    timeout=30,  # Thêm timeout 30 giây
                )
                return result
            except facebook.GraphAPIError as e:
                logger.error(
                    f"Facebook GraphAPI Error (attempt {attempt + 1}/{max_retries}): {e}"
                )
                if attempt == max_retries - 1:  # Lần cuối
                    raise HTTPException(
                        status_code=500,
                        detail=f"Failed to send message to Facebook: {e}",
                    )
                time.sleep(retry_delay)
                retry_delay *= 2  # Exponential backoff
            except Exception as e:
                logger.error(
                    f"Unexpected error sending message to Facebook (attempt {attempt + 1}/{max_retries}): {e}"
                )
                if attempt == max_retries - 1:  # Lần cuối
                    raise HTTPException(
                        status_code=500, detail="Failed to send message to Facebook"
                    )
                time.sleep(retry_delay)
                retry_delay *= 2  # Exponential backoff

    def _get_page_info_sync(self, page_access_token: str, page_id: str) -> dict:
        """
        Lấy thông tin page sử dụng Facebook SDK.
        """
        max_retries = 3
        retry_delay = 1  # giây

        for attempt in range(max_retries):
            try:
                graph = facebook.GraphAPI(access_token=page_access_token, version="3.1")
                result = graph.get_object(page_id)
                return result
            except facebook.GraphAPIError as e:
                logger.error(
                    f"Facebook GraphAPI Error getting page info (attempt {attempt + 1}/{max_retries}): {e}"
                )
                if attempt == max_retries - 1:  # Lần cuối
                    raise HTTPException(
                        status_code=500, detail=f"Failed to get page info: {e}"
                    )
                time.sleep(retry_delay)
                retry_delay *= 2  # Exponential backoff
            except Exception as e:
                logger.error(
                    f"Unexpected error getting page info (attempt {attempt + 1}/{max_retries}): {e}"
                )
                if attempt == max_retries - 1:  # Lần cuối
                    raise HTTPException(
                        status_code=500, detail="Failed to get page info"
                    )
                time.sleep(retry_delay)
                retry_delay *= 2  # Exponential backoff

    @timing_decorator_async
    async def send_message(
        self,
        page_access_token: Optional[str] = None,
        recipient_id: Optional[str] = None,
        message: str = "",
    ) -> dict:
        page_access_token = page_access_token or self.page_token
        recipient_id = recipient_id or self.sender_id

        if not message or not str(message).strip():
            logger.warning(
                f"[FACEBOOK_SEND] Attempted to send an empty or whitespace-only message to recipient {recipient_id}. Aborting."
            )
            return {}

        if not page_access_token or not recipient_id:
            logger.error(
                f"[FACEBOOK_SEND] Missing page_access_token or recipient_id. Cannot send message."
            )
            raise ValueError(
                "FacebookClient: page_access_token and recipient_id must not be None when sending a message."
            )

        logger.info(
            f"[FACEBOOK_SEND] Preparing to send message to recipient {recipient_id}. Full message (truncated): '{_safe_truncate(str(message))}'"
        )

        # Format message
        response_to_send = self.format_message(str(message).replace("**", "*"))

        # Chia nhỏ nếu quá dài
        messages = self.split_message(response_to_send)
        results = []

        for i, msg_part in enumerate(messages, 1):
            if len(msg_part) > 2000:
                msg_part = msg_part[:2000]  # fallback cắt cứng

            logger.info(
                f"[FACEBOOK_SEND] Sending part {i}/{len(messages)} to recipient {recipient_id}."
            )
            try:
                # Wrap sync HTTP call in thread executor để giữ async
                loop = asyncio.get_event_loop()
                result = await loop.run_in_executor(
                    None,
                    self.send_message_forwarder,
                    page_access_token,
                    recipient_id,
                    msg_part,
                )
                results.append(result)
            except Exception as e:
                logger.error(
                    f"[FACEBOOK_SEND] Failed to send part {i}/{len(messages)} to {recipient_id}. Error: {e}"
                )
                results.append({"error": str(e), "part": i})

        return results[0] if results else {}

    @timing_decorator_async
    async def get_page_info(
        self, page_access_token: Optional[str] = None, page_id: Optional[str] = None
    ) -> dict:
        """
        Lấy thông tin page sử dụng Facebook SDK (async).
        """
        page_access_token = page_access_token or self.page_token
        page_id = page_id or self.page_id
        if not page_access_token or not page_id:
            raise ValueError(
                "FacebookClient: page_access_token and page_id must not be None when getting page info."
            )

        loop = asyncio.get_event_loop()
        result = await loop.run_in_executor(
            None, self._get_page_info_sync, page_access_token, page_id
        )
        return result

    @timing_decorator_sync
    def parse_message(self, body: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        """
        Parse message từ payload Facebook webhook.
        Input: body (dict) - payload JSON từ Facebook.
        Output: dict chứa sender_id, page_id, timestamp, text, attachments hoặc None nếu lỗi.
        """
        try:
            entry = body["entry"][0]
            messaging = entry["messaging"][0]

            sender_id = messaging["sender"]["id"]
            recipient_id = messaging["recipient"]["id"]
            timestamp = messaging["timestamp"]

            message_data = {
                "sender_id": sender_id,
                "page_id": recipient_id,
                "timestamp": timestamp,
                "text": None,
                "attachments": [],
            }

            if "message" in messaging:
                message = messaging["message"]
                if "text" in message:
                    message_data["text"] = message["text"]
                if "attachments" in message:
                    message_data["attachments"] = message["attachments"]

            return message_data
        except (KeyError, IndexError) as e:
            logger.error(f"Error parsing Facebook message: {e}\n\n{body}")
            return None