zioinfo-mail/messenger/routers/webhook.py
DESKTOP-TKLFCPR\ython e228faabf5 feat(itsm): G-1~G-12 확장 기능 + 하네스/봇/설치스크립트 구현
G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현
G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건)
G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST)
G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직
G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트
G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트
G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API
G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트
G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트
G-10: core/push_notify.py + routers/push.py + PushSubscription 모델
G-11: approvals 다중승인 (위임/서명/기한초과/마감연장)
G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh

하네스: guardia-orchestrator 확장기능 Phase 반영
봇명령어: /sr /status /license /bulk 슬래시 명령어 추가
설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:18:52 +09:00

106 lines
3.7 KiB
Python

import hashlib
import hmac
import json
from datetime import datetime
from uuid import uuid4
from fastapi import APIRouter, HTTPException, Request
# ws_relay의 broadcast 함수 임포트 (순환 참조 방지를 위해 지연 임포트)
router = APIRouter(prefix="/api")
WEBHOOK_SECRET = "guardia-webhook-secret-2026"
ITSM_URL = "http://localhost:8001" # ITSM 서버 주소
TYPE_KR = {"DEPLOY":"배포", "RESTART":"재기동", "LOG":"로그 분석",
"INQUIRY":"문의", "OTHER":"기타"}
def _verify(signature: str, body: bytes) -> bool:
expected = "sha256=" + hmac.new(
WEBHOOK_SECRET.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
def _new_msg_id() -> str:
return f"MSG-{datetime.now().strftime('%Y%m%d')}-{str(uuid4())[:8].upper()}"
def _new_sr() -> str:
return f"SR-{datetime.now().strftime('%Y%m%d')}-{str(uuid4())[:6].upper()}"
@router.post("/messenger/webhook")
async def receive_webhook(request: Request):
from routers.ws_relay import broadcast, room_channels
from core.bot import store_message
body = await request.body()
payload = json.loads(body)
# ── ITSM 완료 이벤트 처리 ─────────────────────────────────────────
if payload.get("event") == "itsm_complete":
room = payload.get("room", "ops")
sr_id = payload.get("sr_id", "")
title = payload.get("title", "SR 처리 완료")
s_type = TYPE_KR.get(payload.get("sr_type",""), payload.get("sr_type",""))
req_by = payload.get("requested_by", "고객")
server = payload.get("target_server", "")
summary = payload.get("result_summary", "처리 완료")
content = (
f"✅ **{title}** 처리 완료\n"
f"• SR: `{sr_id}` 유형: {s_type}\n"
f"• 요청자: {req_by} 대상: {server}\n"
f"• 결과: {summary}\n\n"
f"서비스에 만족하셨나요? 아래에서 평가해 주세요."
)
bot_msg = {
"message_id": _new_msg_id(),
"timestamp": datetime.now().isoformat(),
"room_id": room,
"sender": "GUARDiA-Bot",
"sender_type": "BOT",
"msg_type": "CHAT",
"content": content,
"is_widget": True,
"interactive_action": {
"type": "STAR_RATING",
"sr_id": sr_id,
"customer": req_by,
"itsm_url": ITSM_URL,
},
}
store_message(room, bot_msg)
await broadcast(room, bot_msg)
return {"status": "NOTIFIED", "room": room, "sr_id": sr_id}
# ── 일반 웹훅 (기존 로직) ─────────────────────────────────────────
if payload.get("bot") or not payload.get("text", "").strip():
return {"status": "IGNORED"}
sr_id = _new_sr()
return {"status": "ACCEPTED", "sr_id": sr_id}
@router.post("/sr/approve/{sr_id}")
async def approve_sr(sr_id: str):
return {"status": "APPROVED", "sr_id": sr_id, "message": f"SR {sr_id} 승인 완료 — 실행을 시작합니다."}
@router.post("/sr/reject/{sr_id}")
async def reject_sr(sr_id: str):
return {"status": "REJECTED", "sr_id": sr_id, "message": f"SR {sr_id} 반려 처리되었습니다."}
@router.post("/killswitch/confirm")
async def killswitch_confirm():
return {"status": "STOPPED", "message": "모든 진행 중인 작업이 중단되었습니다."}
@router.post("/killswitch/cancel")
async def killswitch_cancel():
return {"status": "CANCELLED", "message": "Kill-Switch 취소되었습니다."}