Files

240 lines
8.9 KiB
Python

"""텔레그램 Webhook 이벤트 처리."""
from typing import Optional
from ..db import get_telegram_callback, mark_telegram_responded
from .client import _enabled, api_call
async def handle_webhook(data: dict, agent_dispatcher=None) -> Optional[dict]:
"""텔레그램에서 들어오는 이벤트 처리.
- callback_query(인라인 버튼)는 항상 처리 → 승인/거절 dict 반환
- message(텍스트 슬래시 명령)는 `agent_dispatcher`가 주입된 경우에만 처리
agent_dispatcher: async (agent_id, command, params) -> dict
- agent_id == "__global__", command == "status" 특수 케이스는
{agent_id: {state, detail}} dict를 반환해야 함.
"""
callback_query = data.get("callback_query")
if callback_query:
return await _handle_callback(callback_query)
message = data.get("message")
if message:
chat = message.get("chat", {})
print(f"[TG-WEBHOOK] chat.id={chat.get('id')} type={chat.get('type')} text={message.get('text')!r}", flush=True)
if message and message.get("text") and agent_dispatcher is not None:
return await _handle_message(message, agent_dispatcher)
return None
async def _handle_callback(callback_query: dict) -> Optional[dict]:
"""승인/거절 및 realestate 북마크 콜백 처리."""
callback_id = callback_query.get("data", "")
# realestate 북마크 토글 콜백 — DB 조회 없이 직접 처리
if callback_id.startswith("realestate_bookmark_"):
return await _handle_realestate_bookmark(callback_query, callback_id)
if callback_id.startswith("render_"):
return await _handle_insta_render(callback_query, callback_id)
cb = get_telegram_callback(callback_id)
if not cb:
return None
action = callback_id.split("_")[0]
mark_telegram_responded(callback_id, action)
feedback_text = {
"approve": "승인됨 ✅",
"reject": "거절됨 ❌",
}.get(action, f"처리됨: {action}")
await api_call(
"answerCallbackQuery",
{
"callback_query_id": callback_query["id"],
"text": feedback_text,
},
)
return {
"task_id": cb["task_id"],
"agent_id": cb["agent_id"],
"action": action,
"approved": action == "approve",
}
async def _handle_realestate_bookmark(callback_query: dict, callback_id: str) -> dict:
"""realestate_bookmark_{announcement_id} 콜백 처리."""
from .. import service_proxy
from .messaging import send_raw
# answerCallbackQuery 먼저 — 텔레그램 로딩 스피너 해제
await api_call(
"answerCallbackQuery",
{"callback_query_id": callback_query["id"], "text": "처리 중..."},
)
try:
ann_id = int(callback_id.removeprefix("realestate_bookmark_"))
except ValueError:
await send_raw("⚠️ 잘못된 북마크 콜백 데이터")
return {"ok": False, "error": "invalid_callback_data"}
try:
result = await service_proxy.realestate_bookmark_toggle(ann_id)
is_on = result.get("is_bookmarked")
if is_on == 1:
await send_raw(f"🔖 북마크 추가 완료 (#{ann_id})")
elif is_on == 0:
await send_raw(f"🔖 북마크 해제 완료 (#{ann_id})")
else:
await send_raw(f"🔖 북마크 토글 완료 (#{ann_id})")
return {"ok": True, "announcement_id": ann_id}
except Exception as e:
await send_raw(f"⚠️ 북마크 처리 실패: {e}")
return {"ok": False, "error": str(e)}
async def _handle_insta_render(callback_query: dict, callback_id: str) -> dict:
"""render_{keyword_id} 콜백 → InstaAgent.on_callback('render', ...).
텔레그램 인라인 버튼이 보낸 callback_data가 `render_<keyword_id>` 형식.
InstaAgent._push_keyword_candidates가 callback_data를 그대로 박아 보내며,
별도 DB lookup 없이 keyword_id를 파싱해 dispatch한다."""
from .messaging import send_raw
from ..agents import AGENT_REGISTRY
await api_call(
"answerCallbackQuery",
{"callback_query_id": callback_query["id"], "text": "카드 생성 시작"},
)
try:
keyword_id = int(callback_id.removeprefix("render_"))
except ValueError:
await send_raw("⚠️ 잘못된 render 콜백 데이터")
return {"ok": False, "error": "invalid_callback_data"}
agent = AGENT_REGISTRY.get("insta")
if not agent:
await send_raw("⚠️ insta agent 미등록")
return {"ok": False, "error": "agent_missing"}
try:
return await agent.on_callback("render", {"keyword_id": keyword_id})
except Exception as e:
await send_raw(f"⚠️ 카드 생성 실패: {e}")
return {"ok": False, "error": str(e)}
async def _handle_message(message: dict, agent_dispatcher) -> Optional[dict]:
"""슬래시 명령 메시지 처리."""
from .router import parse_command, resolve_agent_command, HELP_TEXT
from .messaging import send_raw, send_agent_message
from .agent_registry import AGENT_META
from .conversational import maybe_route_to_pipeline
# 파이프라인 메시지에 대한 reply라면 youtube_publisher 로 라우팅
if await maybe_route_to_pipeline(message):
return {"handled": "pipeline_reply"}
text = message.get("text", "")
parsed = parse_command(text)
if not parsed:
# 슬래시 명령이 아니면 자연어 대화로 라우팅
chat_id = str(message.get("chat", {}).get("id", ""))
if not chat_id:
return None
from .conversational import respond_to_message
reply = await respond_to_message(chat_id, text)
if reply:
import html as _html
await send_raw(_html.escape(reply), chat_id=chat_id)
return {"handled": "chat"}
return None
agent_id, command, args = parsed
# 전역 명령
if agent_id is None:
if command == "help":
await send_raw(HELP_TEXT)
return {"handled": "help"}
if command == "agents":
lines = ["<b>📋 등록된 에이전트</b>", ""]
for aid, meta in AGENT_META.items():
lines.append(
f"{meta['emoji']} <b>{meta['display_name']}</b> <code>/{aid}</code>"
)
await send_raw("\n".join(lines))
return {"handled": "agents"}
if command == "status":
try:
result = await agent_dispatcher("__global__", "status", {})
body_lines = []
if isinstance(result, dict):
for aid, info in result.items():
meta = AGENT_META.get(
aid, {"emoji": "🤖", "display_name": aid}
)
state = info.get("state", "unknown") if isinstance(info, dict) else "unknown"
body_lines.append(
f"{meta['emoji']} <b>{meta['display_name']}</b>: <code>{state}</code>"
)
detail = info.get("detail") if isinstance(info, dict) else None
if detail:
body_lines.append(f"{detail}")
await send_raw("<b>📊 전체 상태</b>\n\n" + "\n".join(body_lines))
except Exception as e:
await send_raw(f"⚠️ 상태 조회 실패: {e}")
return {"handled": "status"}
return None
# 에이전트 명령
if agent_id not in AGENT_META:
await send_raw(
f"⚠️ 알 수 없는 에이전트: <code>{agent_id}</code>\n/help 로 사용 가능한 명령 확인"
)
return {"handled": "unknown_agent"}
resolved = resolve_agent_command(agent_id, command, args)
if resolved is None:
await send_raw(
f"⚠️ <code>{agent_id}</code>에서 <code>{command}</code> 명령은 지원하지 않습니다."
)
return {"handled": "unknown_command"}
internal_cmd, params = resolved
try:
result = await agent_dispatcher(agent_id, internal_cmd, params)
ok = result.get("ok", False) if isinstance(result, dict) else False
msg = result.get("message", "") if isinstance(result, dict) else str(result)
await send_agent_message(
agent_id=agent_id,
kind="info" if ok else "error",
title=f"{internal_cmd} 실행 결과",
body=msg or str(result),
)
except Exception as e:
await send_raw(f"⚠️ 명령 실행 실패: {e}")
return {"handled": "command", "agent_id": agent_id, "command": internal_cmd}
async def setup_webhook() -> dict:
from ..config import TELEGRAM_WEBHOOK_URL
if not _enabled() or not TELEGRAM_WEBHOOK_URL:
return {"ok": False, "description": "Webhook URL not configured"}
return await api_call("setWebhook", {"url": TELEGRAM_WEBHOOK_URL})