"""Notification dispatcher: Telegram or KakaoTalk with Markdown fallback.""" from __future__ import annotations from datetime import date from typing import TYPE_CHECKING from loguru import logger from gimme_job.config import GlobalConfig if TYPE_CHECKING: from gimme_job.models.db import JobPosting _TELEGRAM_MAX_LEN = 4000 # leave room for safety margin def build_listing_messages(postings: list["JobPosting"], run_started_at=None) -> list[str]: """Format job postings as Telegram HTML chunks (≤4000 chars each).""" from collections import defaultdict from datetime import timezone by_site: dict[str, list] = defaultdict(list) for p in postings: by_site[p.site_id].append(p) total = len(postings) if run_started_at is not None: # Convert UTC to local time for display try: import zoneinfo tz = zoneinfo.ZoneInfo("America/Phoenix") local_dt = run_started_at.replace(tzinfo=timezone.utc).astimezone(tz) dt_str = local_dt.strftime("%Y-%m-%d %H:%M") except Exception: dt_str = run_started_at.strftime("%Y-%m-%d %H:%M") header = f"📋 신규 채용 공고 {total}개 {dt_str}\n" else: header = f"📋 신규 채용 공고 {total}개\n" chunks: list[str] = [] current = header for site_id, site_postings in sorted(by_site.items()): site_header = f"\n── {site_id.upper()} ({len(site_postings)}) ──\n" for p in site_postings: title = _escape_html(p.title) line = f"• {title}" details = [] if p.company: details.append(_escape_html(p.company)) if p.location: details.append(_escape_html(p.location)) if p.employment_type: details.append(_escape_html(p.employment_type)) if details: line += f"\n {' | '.join(details)}" if p.job_url: safe_url = p.job_url.replace("&", "&") line += f'\n 링크' line += "\n" # Add site header before the first card of each site prefix = site_header if site_header else "" candidate = current + prefix + line if len(candidate) > _TELEGRAM_MAX_LEN: # Flush current chunk and start new one chunks.append(current.rstrip()) current = prefix + line else: current = candidate site_header = "" # Only prepend once per site if current.strip(): chunks.append(current.rstrip()) return chunks or ["신규 채용 공고가 없습니다."] def _escape_html(text: str) -> str: return text.replace("&", "&").replace("<", "<").replace(">", ">") class NotificationDispatcher: def __init__(self, global_config: GlobalConfig, session_factory): self.cfg = global_config self.session_factory = session_factory def send(self, summary: str, run_date: date) -> bool: """Send the summary. Returns True if any method succeeded.""" provider = self.cfg.notification.provider success = False if provider == "kakaotalk": from gimme_job.runtime.kakao import KakaoTalkClient client = KakaoTalkClient() if client.is_configured(): ok = client.send_self_memo(summary) if ok: success = True self._log_notification(run_date, "kakaotalk", "success") else: logger.warning("KakaoTalk failed — writing markdown fallback") self._log_notification(run_date, "kakaotalk", "failed", "send_self_memo returned False") else: logger.warning("KakaoTalk not configured — writing markdown fallback only") self._log_notification(run_date, "kakaotalk", "skipped", "not configured") else: # telegram (default) from gimme_job.runtime.telegram import TelegramClient client = TelegramClient() if client.is_configured(): ok = client.send_message(summary) if ok: success = True self._log_notification(run_date, "telegram", "success") else: logger.warning("Telegram failed — writing markdown fallback") self._log_notification(run_date, "telegram", "failed", "send_message returned False") else: logger.warning("Telegram not configured — writing markdown fallback only") self._log_notification(run_date, "telegram", "skipped", "not configured") if self.cfg.notification.fallback_markdown: path = self.send_markdown_fallback(summary, run_date) logger.info(f"Markdown saved: {path}") if not success: self._log_notification(run_date, "markdown", "success") success = True return success def send_markdown_fallback(self, summary: str, run_date: date): """Save summary to workspace/reports/YYYY-MM-DD.md.""" from gimme_job.utils.paths import reports_dir path = reports_dir() / f"{run_date.strftime('%Y-%m-%d')}.md" path.parent.mkdir(parents=True, exist_ok=True) path.write_text(summary, encoding="utf-8") return path def _log_notification( self, run_date: date, provider: str, status: str, error_message: str | None = None, ) -> None: try: with self.session_factory() as session: from gimme_job.db.repo import NotificationLogRepo NotificationLogRepo().log_notification( session, run_date, provider, status, error_message ) except Exception as e: logger.warning(f"Failed to log notification: {e}")