79 lines
2.5 KiB
Python
79 lines
2.5 KiB
Python
"""Synology Chat webhook — outgoing webhook 수신 + 파이프라인 연결."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
|
|
from fastapi import APIRouter, Request
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from config import settings
|
|
from services.job_manager import job_manager
|
|
from services import job_queue as jq_module
|
|
from services.state_stream import state_stream
|
|
from services.synology_sender import send_to_synology
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(tags=["synology"])
|
|
|
|
# 중복 요청 방지 (retry 대비) — {user_id}:{timestamp} → expire time
|
|
_recent: dict[str, float] = {}
|
|
DEDUP_TTL = 30.0 # 30초 내 동일 요청 무시
|
|
|
|
|
|
def _cleanup_recent():
|
|
now = time.time()
|
|
expired = [k for k, v in _recent.items() if now - v > DEDUP_TTL]
|
|
for k in expired:
|
|
del _recent[k]
|
|
|
|
|
|
@router.post("/webhook/synology")
|
|
async def synology_webhook(request: Request):
|
|
"""Synology Chat outgoing webhook 수신."""
|
|
if not settings.synology_incoming_url:
|
|
return JSONResponse(status_code=503, content={"detail": "Synology integration disabled"})
|
|
|
|
# Parse form data
|
|
form = await request.form()
|
|
token = form.get("token", "")
|
|
text = form.get("text", "")
|
|
username = form.get("username", "")
|
|
user_id = form.get("user_id", "")
|
|
timestamp = form.get("timestamp", "")
|
|
|
|
# Token 검증
|
|
if token != settings.synology_outgoing_token:
|
|
logger.warning("Invalid Synology token from %s", username)
|
|
return JSONResponse(status_code=403, content={"detail": "Invalid token"})
|
|
|
|
if not text or not text.strip():
|
|
return JSONResponse(status_code=200, content={"text": "빈 메시지입니다."})
|
|
|
|
# 중복 요청 방지
|
|
_cleanup_recent()
|
|
dedup_key = f"{user_id}:{timestamp}"
|
|
if dedup_key in _recent:
|
|
logger.info("Duplicate webhook ignored: %s", dedup_key)
|
|
return JSONResponse(status_code=200, content={})
|
|
_recent[dedup_key] = time.time()
|
|
|
|
# Job 생성
|
|
job = job_manager.create(text.strip())
|
|
job.callback = "synology"
|
|
job.callback_meta = {"username": username, "user_id": user_id}
|
|
state_stream.create(job.id)
|
|
|
|
logger.info("Synology job %s from %s: %s", job.id, username, text[:50])
|
|
|
|
# "처리 중" 메시지 먼저 전송 (typing 느낌)
|
|
await send_to_synology("🤔 생각 중...")
|
|
|
|
# 파이프라인 시작 (비동기)
|
|
await jq_module.job_queue.submit(job)
|
|
|
|
# 즉시 200 반환 (Synology는 빠른 응답 기대)
|
|
return JSONResponse(status_code=200, content={})
|