"""Synology Chat — incoming webhook으로 응답 전송.""" from __future__ import annotations import asyncio import json import logging import re import httpx from config import settings logger = logging.getLogger(__name__) # Synology Chat rate limit 대응: 최소 전송 간격 _last_send_time: float = 0.0 MIN_SEND_INTERVAL = 1.5 # 초 def _strip_markdown(text: str) -> str: """마크다운 문법 제거 — Synology Chat은 렌더링 안 됨.""" text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) text = re.sub(r'\*(.+?)\*', r'\1', text) text = re.sub(r'`(.+?)`', r'\1', text) text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE) text = re.sub(r'^\s*[-*]\s+', '• ', text, flags=re.MULTILINE) return text async def send_to_synology(text: str, *, raw: bool = False) -> bool: """Incoming webhook URL로 메시지 전송. rate limit 대응 포함.""" global _last_send_time if not settings.synology_incoming_url: logger.warning("Synology incoming URL not configured") return False if not raw: text = _strip_markdown(text) payload = json.dumps({"text": text}, ensure_ascii=False) # Rate limit 대응: 최소 간격 보장 import time now = time.time() elapsed = now - _last_send_time if elapsed < MIN_SEND_INTERVAL: await asyncio.sleep(MIN_SEND_INTERVAL - elapsed) try: async with httpx.AsyncClient(verify=False, timeout=10.0) as client: resp = await client.post( settings.synology_incoming_url, data={"payload": payload}, ) _last_send_time = time.time() if resp.status_code == 200: body = resp.json() if resp.text else {} if body.get("success") is False: error_msg = body.get("error", {}).get("errors", "unknown") logger.warning("Synology API error: %s — retrying", error_msg) # 1회 재시도 await asyncio.sleep(2.0) resp2 = await client.post( settings.synology_incoming_url, data={"payload": payload}, ) _last_send_time = time.time() body2 = resp2.json() if resp2.text else {} if body2.get("success") is not False: logger.info("Synology retry sent (%d chars): %s", len(text), text[:100]) return True logger.error("Synology retry also failed: %s", body2) return False logger.info("Synology sent (%d chars): %s", len(text), text[:100]) return True logger.error("Synology send failed: %d %s", resp.status_code, resp.text) return False except Exception: logger.exception("Failed to send to Synology Chat") return False