init project
Browse files- .env.template +4 -0
- .gitignore +2 -1
- Dockerfile +5 -0
- app/config.py +26 -0
- app/handlers/parser.py +48 -0
- app/handlers/webhook.py +33 -0
- app/main.py +5 -0
- app/services/responder.py +15 -0
- app/services/supabase.py +10 -0
- app/utils.py +24 -0
- requirements.txt +4 -0
.env.template
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# .env.template
|
2 |
+
FB_VERIFY_TOKEN=
|
3 |
+
SUPABASE_KEY=
|
4 |
+
VECTOR_API_KEY=
|
.gitignore
CHANGED
@@ -191,4 +191,5 @@ cython_debug/
|
|
191 |
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
192 |
# refer to https://docs.cursor.com/context/ignore-files
|
193 |
.cursorignore
|
194 |
-
.cursorindexingignore
|
|
|
|
191 |
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
192 |
# refer to https://docs.cursor.com/context/ignore-files
|
193 |
.cursorignore
|
194 |
+
.cursorindexingignore.env
|
195 |
+
.DS_Store
|
Dockerfile
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.10-slim
|
2 |
+
WORKDIR /app
|
3 |
+
COPY . .
|
4 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
5 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
app/config.py
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from dotenv import load_dotenv
|
3 |
+
|
4 |
+
# Tải biến môi trường từ file `.env`
|
5 |
+
load_dotenv()
|
6 |
+
|
7 |
+
# === Facebook Webhook ===
|
8 |
+
# Token xác thực khi Facebook gửi GET webhook
|
9 |
+
FB_VERIFY_TOKEN = os.getenv("FB_VERIFY_TOKEN")
|
10 |
+
|
11 |
+
# Địa chỉ gửi tin nhắn Facebook Graph API
|
12 |
+
FB_GRAPH_API = "https://graph.facebook.com/v22.0"
|
13 |
+
|
14 |
+
# === Google Sheets ===
|
15 |
+
# ID bảng tính và tên sheet
|
16 |
+
GOOGLE_SHEET_ID = "1H7BxVgXy5GgzRVxg34TMWN6xgqI4gA8zwzIAIZp2NdI"
|
17 |
+
GOOGLE_SHEET_NAME = "chat"
|
18 |
+
|
19 |
+
# === Supabase ===
|
20 |
+
# Địa chỉ Supabase và key truy cập
|
21 |
+
SUPABASE_URL = "https://hekathwdexaukowdjtxj.supabase.co" # Thay bằng domain thật
|
22 |
+
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
|
23 |
+
|
24 |
+
# === Vector Search (pgvector + Supabase function) ===
|
25 |
+
VECTOR_SEARCH_URL = "https://hekathwdexaukowdjtxj.supabase.co/rest/v1/rpc/match_documents"
|
26 |
+
VECTOR_API_KEY = os.getenv("VECTOR_API_KEY")
|
app/handlers/parser.py
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app.utils import log_task
|
2 |
+
|
3 |
+
@log_task("Phân tích tin nhắn")
|
4 |
+
def parse_message(message: dict) -> dict:
|
5 |
+
"""
|
6 |
+
Args:
|
7 |
+
message: JSON từ Messenger
|
8 |
+
Returns:
|
9 |
+
dict chứa sender, page_id, text, attachments, command, content, vehicle
|
10 |
+
"""
|
11 |
+
t = message.get("message", {}).get("text", "")
|
12 |
+
attachments = message.get("message", {}).get("attachments", [])
|
13 |
+
sid = message.get("sender", {}).get("id")
|
14 |
+
pid = message.get("recipient", {}).get("id")
|
15 |
+
|
16 |
+
res = {
|
17 |
+
"text": t,
|
18 |
+
"attachments": attachments,
|
19 |
+
"recipient_id": sid,
|
20 |
+
"page_id": pid,
|
21 |
+
"command": "",
|
22 |
+
"password": "",
|
23 |
+
"content": "",
|
24 |
+
"vehicle": detect_vehicle(t.lower())
|
25 |
+
}
|
26 |
+
|
27 |
+
if t.startswith("\\"):
|
28 |
+
parts = t[1:].split("\\")
|
29 |
+
res.update({
|
30 |
+
"command": parts[0].strip(),
|
31 |
+
"password": parts[1].strip() if len(parts) > 1 else "",
|
32 |
+
"content": "\\".join(parts[2:]).strip() if len(parts) > 2 else ""
|
33 |
+
})
|
34 |
+
|
35 |
+
return res
|
36 |
+
|
37 |
+
def detect_vehicle(text: str) -> str:
|
38 |
+
"""
|
39 |
+
Args:
|
40 |
+
text: nội dung tin nhắn
|
41 |
+
Returns:
|
42 |
+
một trong các phương tiện, hoặc "" nếu không tìm thấy
|
43 |
+
"""
|
44 |
+
keywords = ["mô tô","xe máy điện","xe máy","ô tô","xe đạp","đi bộ"]
|
45 |
+
for k in keywords:
|
46 |
+
if k in text:
|
47 |
+
return k
|
48 |
+
return ""
|
app/handlers/webhook.py
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Request
|
2 |
+
from app.config import FB_VERIFY_TOKEN
|
3 |
+
from app.handlers.parser import parse_message
|
4 |
+
from app.services.supabase import get_page_token
|
5 |
+
from app.services.responder import send_fallback_message
|
6 |
+
|
7 |
+
router = APIRouter()
|
8 |
+
|
9 |
+
@router.get("/facebook-webhook")
|
10 |
+
def verify_token(request: Request):
|
11 |
+
q = dict(request.query_params)
|
12 |
+
if q.get("hub.mode") == "subscribe" and q.get("hub.verify_token") == FB_VERIFY_TOKEN:
|
13 |
+
return q.get("hub.challenge")
|
14 |
+
return {"error": "Invalid token"}
|
15 |
+
|
16 |
+
@router.post("/facebook-webhook")
|
17 |
+
async def handle_message(request: Request):
|
18 |
+
body = await request.json()
|
19 |
+
try:
|
20 |
+
ev = body["entry"][0]["messaging"][0]
|
21 |
+
if ev.get("message", {}).get("is_echo"):
|
22 |
+
return {"status": "echo ignored"}
|
23 |
+
|
24 |
+
parsed = parse_message(ev)
|
25 |
+
parsed["token"] = get_page_token(parsed["page_id"])
|
26 |
+
|
27 |
+
if not parsed.get("vehicle"):
|
28 |
+
send_fallback_message(parsed["page_id"], parsed["recipient_id"], parsed["token"])
|
29 |
+
return {"status": "fallback sent"}
|
30 |
+
|
31 |
+
return {"status": "ok"}
|
32 |
+
except Exception as e:
|
33 |
+
return {"error": str(e)}
|
app/main.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI
|
2 |
+
from app.handlers.webhook import router
|
3 |
+
|
4 |
+
app = FastAPI()
|
5 |
+
app.include_router(router)
|
app/services/responder.py
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
from app.utils import log_task
|
3 |
+
from app.config import FB_GRAPH_API
|
4 |
+
|
5 |
+
@log_task("Gửi fallback message")
|
6 |
+
def send_fallback_message(page_id: str, recipient_id: str, token: str):
|
7 |
+
url = f"{FB_GRAPH_API}/{page_id}/messages"
|
8 |
+
data = {
|
9 |
+
"recipient": {"id": recipient_id},
|
10 |
+
"messaging_type": "RESPONSE",
|
11 |
+
"message": {"text": "Tôi chưa rõ phương tiện bạn đang sử dụng. Bạn có thể cung cấp thêm không?"}
|
12 |
+
}
|
13 |
+
params = {"access_token": token}
|
14 |
+
r = requests.post(url, json=data, params=params)
|
15 |
+
r.raise_for_status()
|
app/services/supabase.py
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app.utils import log_task
|
2 |
+
from app.config import SUPABASE_URL, SUPABASE_KEY
|
3 |
+
|
4 |
+
@log_task("Lấy token page từ Supabase")
|
5 |
+
def get_page_token(page_id: str) -> str:
|
6 |
+
"""
|
7 |
+
Giả lập hoặc thực gọi Supabase để lấy token
|
8 |
+
"""
|
9 |
+
dummy = {"123": "fake_token"}
|
10 |
+
return dummy.get(page_id, "unknown_token")
|
app/utils.py
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import time
|
2 |
+
import logging
|
3 |
+
from functools import wraps
|
4 |
+
|
5 |
+
logging.basicConfig(level=logging.INFO)
|
6 |
+
|
7 |
+
def log_task(name):
|
8 |
+
"""
|
9 |
+
Decorator log thông tin task: bắt đầu, kết thúc, lỗi, thời gian.
|
10 |
+
"""
|
11 |
+
def decorator(func):
|
12 |
+
@wraps(func)
|
13 |
+
def wrapper(*args, **kwargs):
|
14 |
+
logging.info(f"🔵 Bắt đầu: {name}")
|
15 |
+
start = time.time()
|
16 |
+
try:
|
17 |
+
result = func(*args, **kwargs)
|
18 |
+
logging.info(f"🟢 Kết thúc: {name} trong {time.time() - start:.2f}s")
|
19 |
+
return result
|
20 |
+
except Exception as e:
|
21 |
+
logging.error(f"🔴 Lỗi ở {name}: {e}", exc_info=True)
|
22 |
+
return {"error": str(e)}
|
23 |
+
return wrapper
|
24 |
+
return decorator
|
requirements.txt
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
fastapi
|
2 |
+
uvicorn
|
3 |
+
requests
|
4 |
+
python-dotenv
|