#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import xml.etree.ElementTree as ET
import logging
from typing import Dict, List, Optional
logger = logging.getLogger(__name__)
class XMLParser:
"""Toast Notification XML 파서"""
def parse_toast_xml(self, xml_string: str) -> Optional[Dict]:
"""
Toast XML 파싱
Args:
xml_string: XML 문자열
Returns:
파싱된 내용 딕셔너리
"""
if not xml_string or not xml_string.strip():
return None
try:
# XML 파싱
root = ET.fromstring(xml_string)
result = {
"type": "toast",
"texts": [],
"images": [],
"actions": [],
"audio": None,
"raw_xml": xml_string[:500] # 처음 500자만
}
# Visual 요소 파싱
visual = root.find('visual')
if visual is not None:
binding = visual.find('binding')
if binding is not None:
# 텍스트 추출
texts = binding.findall('text')
for text_elem in texts:
if text_elem.text:
result["texts"].append(text_elem.text.strip())
# 이미지 추출
images = binding.findall('image')
for img_elem in images:
img_info = {
"src": img_elem.get('src'),
"placement": img_elem.get('placement'),
"hint-crop": img_elem.get('hint-crop')
}
result["images"].append(img_info)
# Actions 파싱
actions_elem = root.find('actions')
if actions_elem is not None:
actions = actions_elem.findall('action')
for action in actions:
action_info = {
"type": action.get('activationType'),
"arguments": action.get('arguments'),
"content": action.get('content'),
"imageUri": action.get('imageUri')
}
result["actions"].append(action_info)
# Audio 파싱
audio_elem = root.find('audio')
if audio_elem is not None:
result["audio"] = {
"src": audio_elem.get('src'),
"silent": audio_elem.get('silent') == 'true'
}
# 내용 요약 생성
result["summary"] = self._generate_summary(result)
return result
except ET.ParseError as e:
logger.warning(f"Failed to parse XML: {e}")
# XML 파싱 실패 시 텍스트 추출 시도
return self._fallback_text_extraction(xml_string)
except Exception as e:
logger.error(f"Unexpected error parsing XML: {e}")
return None
def _generate_summary(self, parsed: Dict) -> str:
"""파싱된 내용에서 요약 생성"""
parts = []
# 텍스트 조합
if parsed["texts"]:
# 첫 번째 텍스트는 보통 제목
title = parsed["texts"][0] if len(parsed["texts"]) > 0 else ""
# 나머지는 본문
body = " ".join(parsed["texts"][1:]) if len(parsed["texts"]) > 1 else ""
if title:
parts.append(f"Title: {title}")
if body:
parts.append(f"Body: {body}")
# 액션 정보
if parsed["actions"]:
action_texts = [a.get("content", "") for a in parsed["actions"] if a.get("content")]
if action_texts:
parts.append(f"Actions: {', '.join(action_texts)}")
return " | ".join(parts) if parts else "No content"
def _fallback_text_extraction(self, xml_string: str) -> Dict:
"""XML 파싱 실패 시 텍스트 추출"""
import re
# <text> 태그 내용 추출
text_pattern = r'<text[^>]*>(.*?)</text>'
texts = re.findall(text_pattern, xml_string, re.DOTALL)
# HTML 엔티티 디코딩
texts = [self._decode_entities(t.strip()) for t in texts if t.strip()]
return {
"type": "toast",
"texts": texts,
"images": [],
"actions": [],
"audio": None,
"summary": " | ".join(texts) if texts else "Unable to parse",
"raw_xml": xml_string[:500]
}
def _decode_entities(self, text: str) -> str:
"""HTML 엔티티 디코딩"""
entities = {
'<': '<',
'>': '>',
'&': '&',
'"': '"',
''': "'"
}
for entity, char in entities.items():
text = text.replace(entity, char)
return text
def extract_email_info(self, parsed: Dict) -> Optional[Dict]:
"""이메일 알림에서 정보 추출"""
if not parsed or not parsed.get("texts"):
return None
texts = parsed["texts"]
# 이메일 패턴 (보통 첫 줄: 발신자, 두 번째 줄: 제목, 세 번째 줄: 본문)
email_info = {
"from": texts[0] if len(texts) > 0 else None,
"subject": texts[1] if len(texts) > 1 else None,
"preview": texts[2] if len(texts) > 2 else None
}
return email_info
def extract_message_info(self, parsed: Dict) -> Optional[Dict]:
"""메시지 알림에서 정보 추출"""
if not parsed or not parsed.get("texts"):
return None
texts = parsed["texts"]
# 메시지 패턴 (보통 첫 줄: 발신자, 두 번째 줄: 메시지)
message_info = {
"sender": texts[0] if len(texts) > 0 else None,
"message": texts[1] if len(texts) > 1 else None
}
return message_info
def is_email_notification(self, parsed: Dict) -> bool:
"""이메일 알림 여부 판단"""
if not parsed:
return False
summary = parsed.get("summary", "").lower()
email_keywords = ["email", "mail", "message from", "@"]
return any(keyword in summary for keyword in email_keywords)
def is_message_notification(self, parsed: Dict) -> bool:
"""메시지 알림 여부 판단"""
if not parsed:
return False
summary = parsed.get("summary", "").lower()
message_keywords = ["slack", "teams", "discord", "telegram", "whatsapp", "message"]
return any(keyword in summary for keyword in message_keywords)
def contains_sensitive_keywords(self, parsed: Dict, keywords: List[str]) -> bool:
"""민감한 키워드 포함 여부"""
if not parsed:
return False
content = parsed.get("summary", "").lower()
return any(keyword.lower() in content for keyword in keywords)