549545bde6
설정·스크립트·스킬·문서·큐레이션 메모리 추적. 시크릿(credentials/identity)·런타임 상태(state/logs/sessions/sqlite)· 백업(clobbered/bak)·dream 캐시는 .gitignore로 제외. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
116 lines
4.4 KiB
Python
116 lines
4.4 KiB
Python
"""Telegram notifier for 골디(budget) — 후잉 동기화 실패 알림 전송.
|
||
|
||
openclaw.json 의 channels.telegram.accounts.budget.botToken 과 allowFrom[0] (관리자 chat_id)
|
||
를 사용해 직접 sendMessage 호출. 외부 의존성 없음.
|
||
"""
|
||
import json
|
||
import re
|
||
import urllib.parse
|
||
import urllib.request
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from zoneinfo import ZoneInfo
|
||
|
||
CONFIG_PATH = Path("/Users/snowoyh/.openclaw/openclaw.json")
|
||
KST = ZoneInfo("Asia/Seoul")
|
||
|
||
|
||
def _load_budget_account():
|
||
cfg = json.loads(CONFIG_PATH.read_text())
|
||
acct = cfg["channels"]["telegram"]["accounts"]["budget"]
|
||
token = acct["botToken"]
|
||
chat_ids = acct.get("allowFrom") or []
|
||
return token, chat_ids
|
||
|
||
|
||
def send(text: str, parse_mode: str = "HTML") -> bool:
|
||
"""관리자에게 텔레그램 메시지 전송. 성공 시 True. 실패해도 예외 안 던짐 (알림 자체로 흐름 막지 않음)."""
|
||
try:
|
||
token, chat_ids = _load_budget_account()
|
||
except Exception as e:
|
||
print(f"⚠️ notify: config 로드 실패: {e}")
|
||
return False
|
||
if not chat_ids:
|
||
print("⚠️ notify: allowFrom 비어 있어 전송 대상 없음")
|
||
return False
|
||
|
||
ok = True
|
||
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
||
for chat_id in chat_ids:
|
||
payload = {
|
||
"chat_id": chat_id,
|
||
"text": text[:4000],
|
||
"parse_mode": parse_mode,
|
||
"disable_web_page_preview": "true",
|
||
}
|
||
data = urllib.parse.urlencode(payload).encode()
|
||
req = urllib.request.Request(url, data=data, method="POST")
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=10) as r:
|
||
body = r.read().decode("utf-8", errors="replace")
|
||
if r.status != 200:
|
||
print(f"⚠️ notify: telegram {r.status}: {body[:200]}")
|
||
ok = False
|
||
except Exception as e:
|
||
print(f"⚠️ notify: telegram 전송 실패: {e}")
|
||
ok = False
|
||
return ok
|
||
|
||
|
||
def escape_html(s: str) -> str:
|
||
return (s.replace("&", "&").replace("<", "<").replace(">", ">"))
|
||
|
||
|
||
def format_kst(iso: str) -> str:
|
||
"""imsg created_at(UTC ISO) -> 'MM-DD HH:MM' KST. 파싱 실패 시 원문."""
|
||
if not iso:
|
||
return ""
|
||
try:
|
||
dt = datetime.fromisoformat(iso.replace("Z", "+00:00")).astimezone(KST)
|
||
return dt.strftime("%m-%d %H:%M")
|
||
except Exception:
|
||
return iso[:16]
|
||
|
||
|
||
def clean_reason(body: str, status: int) -> str:
|
||
"""후잉 응답 본문 -> 한국어 한 줄 사유. 모르는 패턴은 원문 그대로."""
|
||
body = (body or "").strip()
|
||
if status == 0:
|
||
return f"네트워크 오류 — {body[:200]}"
|
||
if not (200 <= status < 300):
|
||
return f"HTTP {status} — {body[:200]}"
|
||
if body.lower() == "fail":
|
||
return "후잉이 형식 거절함 (사유 미제공). 보통 memo 누락 또는 인코딩 문제."
|
||
|
||
# "Error :" / "Error:" 접두어 제거
|
||
m = re.match(r"^Error\s*:?\s*(.+)$", body, flags=re.S)
|
||
inner = (m.group(1).strip() if m else body).strip()
|
||
|
||
# 알려진 패턴 → 한국어 번역
|
||
# 1) "left/right account does not exist at entry_date. ... [계정명]"
|
||
m = re.search(r"(left|right)\s+account\s+does\s+not\s+exist.*?\[(.+?)\]", inner, flags=re.I | re.S)
|
||
if m:
|
||
side = "차변" if m.group(1).lower() == "left" else "대변"
|
||
name = m.group(2).strip()
|
||
return f"{side} 계정 ‘{name}’ 이(가) 후잉 차트에 없습니다. 계정명 오타이거나 거래일에 닫혀있는 계정입니다."
|
||
|
||
# 2) "[필드명] 필드가 없습니다"
|
||
m = re.search(r"\[(\w+)\]\s*필드가\s*없습니다", inner)
|
||
if m:
|
||
return f"필수 필드 ‘{m.group(1)}’ 이(가) 빠졌습니다."
|
||
|
||
# 3) entry_date 관련
|
||
if re.search(r"entry_date.*(format|invalid|올바)", inner, flags=re.I):
|
||
return "거래일(entry_date) 형식 오류. YYYYMMDD 8자리여야 합니다."
|
||
|
||
# 4) money 관련
|
||
if re.search(r"money.*(invalid|format|숫자|negative)", inner, flags=re.I):
|
||
return "금액(money) 값이 잘못됐습니다. 양의 정수만 허용됩니다."
|
||
|
||
# 5) section / webhook URL 만료
|
||
if re.search(r"section|webhook|invalid\s*url|expired", inner, flags=re.I):
|
||
return f"웹훅/섹션 문제 — {inner[:200]}"
|
||
|
||
# 알려진 패턴 없음: 원문 (영문 그대로)
|
||
return inner[:300]
|