173 lines
6.3 KiB
Python
173 lines
6.3 KiB
Python
import threading
|
|
import time
|
|
|
|
import requests
|
|
|
|
from .common import logger
|
|
from .constants import (
|
|
TELEGRAM_MAX_MESSAGE_LENGTH,
|
|
TELEGRAM_RATE_LIMIT_DELAY,
|
|
TELEGRAM_REQUEST_TIMEOUT,
|
|
)
|
|
|
|
__all__ = ["send_telegram", "send_telegram_with_retry", "report_error", "send_startup_test_message"]
|
|
|
|
|
|
def send_telegram_with_retry(
|
|
token: str,
|
|
chat_id: str,
|
|
text: str,
|
|
add_thread_prefix: bool = True,
|
|
parse_mode: str = None,
|
|
max_retries: int | None = None,
|
|
) -> bool:
|
|
"""
|
|
재시도 로직이 포함된 텔레그램 메시지 전송
|
|
|
|
Args:
|
|
token: 텔레그램 봇 토큰
|
|
chat_id: 채팅 ID
|
|
text: 메시지 내용
|
|
add_thread_prefix: 스레드 prefix 추가 여부
|
|
parse_mode: HTML/Markdown 파싱 모드
|
|
max_retries: 최대 재시도 횟수 (None이면 기본값 3)
|
|
|
|
Returns:
|
|
성공 여부 (True/False)
|
|
"""
|
|
if max_retries is None:
|
|
max_retries = 3
|
|
|
|
for attempt in range(max_retries):
|
|
try:
|
|
# 이제 send_telegram은 실패 시 예외를 발생시킴
|
|
send_telegram(token, chat_id, text, add_thread_prefix, parse_mode)
|
|
return True
|
|
except Exception as e:
|
|
if attempt < max_retries - 1:
|
|
wait_time = 2**attempt # Exponential backoff: 1s, 2s, 4s
|
|
logger.warning(
|
|
"텔레그램 전송 실패 (시도 %d/%d), %d초 후 재시도: %s", attempt + 1, max_retries, wait_time, e
|
|
)
|
|
time.sleep(wait_time)
|
|
else:
|
|
logger.error("텔레그램 전송 최종 실패 (%d회 시도): %s", max_retries, e)
|
|
return False
|
|
return False
|
|
|
|
|
|
def send_telegram(
|
|
token: str,
|
|
chat_id: str,
|
|
text: str,
|
|
add_thread_prefix: bool = True,
|
|
parse_mode: str = None,
|
|
max_length: int = TELEGRAM_MAX_MESSAGE_LENGTH,
|
|
):
|
|
"""
|
|
텔레그램 메시지를 전송합니다 (자동 분할 지원).
|
|
|
|
Args:
|
|
token: 텔레그램 봇 토큰
|
|
chat_id: 채팅 ID
|
|
text: 메시지 내용
|
|
add_thread_prefix: 스레드 이름 prefix 추가 여부
|
|
parse_mode: HTML/Markdown 파싱 모드
|
|
max_length: 최대 메시지 길이 (Telegram 제한 4096자, 안전하게 4000자)
|
|
|
|
Note:
|
|
- 메시지가 max_length를 초과하면 자동으로 분할하여 전송합니다.
|
|
- 실패 시 예외를 발생시킵니다.
|
|
- 프로덕션에서는 send_telegram_with_retry() 사용 권장
|
|
"""
|
|
if add_thread_prefix:
|
|
thread_name = threading.current_thread().name
|
|
# 기본 Thread-N 이름이면 prefix 생략 (의미 없는 정보)
|
|
if not thread_name.startswith("Thread-"):
|
|
payload_text = f"[{thread_name}] {text}"
|
|
else:
|
|
payload_text = text
|
|
else:
|
|
payload_text = text
|
|
|
|
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
|
|
|
# ✅ 메시지 길이 확인 및 분할
|
|
if len(payload_text) <= max_length:
|
|
# 단일 메시지 전송
|
|
payload = {"chat_id": chat_id, "text": payload_text}
|
|
if parse_mode:
|
|
payload["parse_mode"] = parse_mode
|
|
|
|
try:
|
|
resp = requests.post(url, json=payload, timeout=TELEGRAM_REQUEST_TIMEOUT)
|
|
resp.raise_for_status()
|
|
logger.debug("텔레그램 메시지 전송 성공: %s", payload_text[:80])
|
|
return True
|
|
except requests.exceptions.Timeout as e:
|
|
logger.warning("텔레그램 타임아웃: %s", e)
|
|
raise
|
|
except requests.exceptions.ConnectionError as e:
|
|
logger.warning("텔레그램 연결 오류: %s", e)
|
|
raise
|
|
except requests.exceptions.HTTPError as e:
|
|
logger.warning("텔레그램 HTTP 오류: %s", e)
|
|
raise
|
|
except requests.exceptions.RequestException as e:
|
|
logger.warning("텔레그램 API 요청 실패: %s", e)
|
|
raise
|
|
else:
|
|
# ✅ 메시지 분할 전송
|
|
chunks = [payload_text[i : i + max_length] for i in range(0, len(payload_text), max_length)]
|
|
logger.info("텔레그램 메시지 길이 초과 (%d자), %d개로 분할 전송", len(payload_text), len(chunks))
|
|
|
|
for i, chunk in enumerate(chunks, 1):
|
|
header = f"[메시지 {i}/{len(chunks)}]\n" if len(chunks) > 1 else ""
|
|
payload = {"chat_id": chat_id, "text": header + chunk}
|
|
if parse_mode:
|
|
payload["parse_mode"] = parse_mode
|
|
|
|
try:
|
|
resp = requests.post(url, json=payload, timeout=TELEGRAM_REQUEST_TIMEOUT)
|
|
resp.raise_for_status()
|
|
logger.debug("텔레그램 분할 메시지 전송 성공 (%d/%d)", i, len(chunks))
|
|
|
|
# Rate Limit 방지
|
|
if i < len(chunks):
|
|
time.sleep(TELEGRAM_RATE_LIMIT_DELAY)
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
logger.error("텔레그램 분할 메시지 전송 실패 (%d/%d): %s", i, len(chunks), e)
|
|
raise
|
|
|
|
return True
|
|
|
|
|
|
def report_error(bot_token: str, chat_id: str, message: str, dry_run: bool):
|
|
"""
|
|
Report an error via Telegram.
|
|
"""
|
|
if not dry_run and bot_token and chat_id:
|
|
# 재시도 로직이 포함된 함수 사용
|
|
send_telegram_with_retry(bot_token, chat_id, message, add_thread_prefix=True)
|
|
|
|
|
|
def send_startup_test_message(bot_token: str, chat_id: str, parse_mode: str, dry_run: bool):
|
|
"""
|
|
Send a startup test message to verify Telegram settings.
|
|
"""
|
|
if dry_run:
|
|
logger.info("[dry-run] Telegram 테스트 메시지 전송 생략")
|
|
return
|
|
|
|
if bot_token and chat_id:
|
|
test_msg = "[테스트] Telegram 설정 확인용 메시지입니다. 봇/채팅 설정이 올바르면 이 메시지가 도착합니다."
|
|
logger.info("텔레그램 테스트 메시지 전송 시도")
|
|
# 재시도 로직이 포함된 함수 사용
|
|
if send_telegram_with_retry(bot_token, chat_id, test_msg, add_thread_prefix=False, parse_mode=parse_mode):
|
|
logger.info("텔레그램 테스트 메시지 전송 성공")
|
|
else:
|
|
logger.warning("텔레그램 테스트 메시지 전송 실패")
|
|
else:
|
|
logger.warning("TELEGRAM_TEST=1 이지만 TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID가 설정되어 있지 않습니다")
|