You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

149 lines
5.4 KiB
Python

"""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"]) -> list[str]:
"""Format job postings as Telegram HTML chunks (≤4000 chars each)."""
from collections import defaultdict
by_site: dict[str, list] = defaultdict(list)
for p in postings:
by_site[p.site_id].append(p)
total = len(postings)
header = f"📋 <b>오늘의 신규 채용 공고 ({total}개)</b>\n"
chunks: list[str] = []
current = header
for site_id, site_postings in sorted(by_site.items()):
site_header = f"\n<b>── {site_id.upper()} ({len(site_postings)}) ──</b>\n"
for p in site_postings:
title = _escape_html(p.title)
line = f"• <b>{title}</b>"
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("&", "&amp;")
line += f'\n <a href="{safe_url}">링크</a>'
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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
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}")