line-webhook / main.py
Therdpoom's picture
Upload 10 files
82eac1b verified
Raw History Blame Contribute Delete
7.02 kB
"""
LINE Official Account Webhook Server
────────────────────────────────────
รับ event จาก LINE → ตรวจสอบ signature → ส่งต่อไปยัง HuggingFace Sentiment API
"""
import hashlib
import hmac
import base64
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
from fastapi.responses import JSONResponse
from app.config import settings
from app.models import LineWebhookPayload
from app.queue import message_queue
from app.sentiment import analyze_sentiment
from app.database import db, init_db
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Startup / Shutdown events"""
logger.info("🚀 Starting LINE Webhook Server...")
await init_db()
yield
logger.info("🛑 Shutting down...")
app = FastAPI(
title="LINE Webhook Sentiment Server",
version="1.0.0",
lifespan=lifespan,
)
# ─── Signature Verification ────────────────────────────────────────────────────
def verify_line_signature(body: bytes, signature: str) -> bool:
"""
ตรวจสอบ X-Line-Signature header จาก LINE
ป้องกัน request ปลอมที่ไม่ได้มาจาก LINE Platform
"""
channel_secret = settings.LINE_CHANNEL_SECRET.encode("utf-8")
hash_value = hmac.new(channel_secret, body, hashlib.sha256).digest()
expected = base64.b64encode(hash_value).decode("utf-8")
return hmac.compare_digest(expected, signature)
# ─── Webhook Endpoint ──────────────────────────────────────────────────────────
@app.post("/webhook")
async def line_webhook(request: Request, background_tasks: BackgroundTasks):
"""
รับ webhook events จาก LINE Platform
Flow:
1. ตรวจสอบ LINE Signature
2. Parse JSON payload
3. กรองเฉพาะ message events (text)
4. ส่งไปประมวลผล sentiment ใน background
"""
# 1. ดึง raw body สำหรับตรวจสอบ signature
body = await request.body()
signature = request.headers.get("X-Line-Signature", "")
if not signature:
logger.warning("⚠️ Missing X-Line-Signature header")
raise HTTPException(status_code=400, detail="Missing signature")
if not verify_line_signature(body, signature):
logger.warning("⚠️ Invalid signature — request rejected")
raise HTTPException(status_code=403, detail="Invalid signature")
# 2. Parse payload
try:
payload = LineWebhookPayload.model_validate_json(body)
except Exception as e:
logger.error(f"❌ Failed to parse payload: {e}")
raise HTTPException(status_code=400, detail="Invalid payload")
# 3. Process events ใน background (ไม่ block LINE server)
for event in payload.events:
if event.type == "message" and event.message.type == "text":
background_tasks.add_task(process_message_event, event)
else:
logger.debug(f"Skipped event type: {event.type}")
# LINE ต้องการ HTTP 200 ทันที ไม่งั้น retry
return JSONResponse(content={"status": "ok"})
# ─── Background Processing ─────────────────────────────────────────────────────
async def process_message_event(event):
"""
ประมวลผลแต่ละ message event:
1. บันทึก raw conversation ลง DB
2. ส่งไป HuggingFace วิเคราะห์ sentiment
3. อัปเดต result กลับใน DB
4. Push ผ่าน queue ไปยัง Dashboard
"""
user_id = event.source.userId
msg_text = event.message.text
conv_id = event.replyToken or f"{user_id}_{event.timestamp}"
logger.info(f"📨 New message | user={user_id} | text={msg_text[:50]}...")
# Step 1 — บันทึก conversation
conversation = await db.save_conversation(
conv_id=conv_id,
user_id=user_id,
message=msg_text,
timestamp=event.timestamp,
source_type=event.source.type,
)
# Step 2 — วิเคราะห์ sentiment ผ่าน HuggingFace
sentiment_result = await analyze_sentiment(msg_text)
logger.info(
f"🧠 Sentiment | label={sentiment_result.label} "
f"score={sentiment_result.score:.3f}"
)
# Step 3 — อัปเดต sentiment ใน DB
await db.update_sentiment(
conv_id=conv_id,
label=sentiment_result.label,
score=sentiment_result.score,
raw_scores=sentiment_result.raw_scores,
)
# Step 4 — Push ไป queue → Dashboard รับแบบ real-time
await message_queue.push({
"conv_id": conv_id,
"user_id": user_id,
"message": msg_text,
"sentiment": sentiment_result.label,
"score": sentiment_result.score,
"timestamp": event.timestamp,
})
# Alert ถ้า negative สูงเกิน threshold
if (
sentiment_result.label == "negative"
and sentiment_result.score >= settings.NEGATIVE_ALERT_THRESHOLD
):
logger.warning(
f"🚨 HIGH NEGATIVE ALERT | user={user_id} | score={sentiment_result.score:.3f}"
)
await message_queue.push_alert({
"type": "high_negative",
"conv_id": conv_id,
"user_id": user_id,
"score": sentiment_result.score,
"message": msg_text,
})
# ─── Health & Dashboard Endpoints ─────────────────────────────────────────────
@app.get("/health")
async def health_check():
"""Health check สำหรับ load balancer / monitoring"""
return {"status": "healthy", "version": app.version}
@app.get("/api/conversations")
async def get_conversations(limit: int = 50, offset: int = 0):
"""
ดึง conversation list พร้อม sentiment สำหรับ Lovable Dashboard
"""
data = await db.get_conversations(limit=limit, offset=offset)
return {"conversations": data, "total": len(data)}
@app.get("/api/sentiment-summary")
async def get_sentiment_summary():
"""
สรุป sentiment ภาพรวมสำหรับ stat cards บน Dashboard
"""
summary = await db.get_sentiment_summary()
return summary