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>
106 lines
3.7 KiB
Python
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 취소되었습니다."}
|