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.
144 lines
5.2 KiB
Python
144 lines
5.2 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_block = f"\n<b>── {site_id.upper()} ({len(site_postings)}) ──</b>\n"
|
|
for p in site_postings:
|
|
# Title line
|
|
title = _escape_html(p.title)
|
|
line = f"• <b>{title}</b>"
|
|
|
|
# Details line
|
|
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)}"
|
|
|
|
# Link
|
|
if p.job_url:
|
|
line += f'\n <a href="{p.job_url}">링크</a>'
|
|
|
|
site_block += line + "\n"
|
|
|
|
# Flush chunk if adding this site would exceed limit
|
|
if len(current) + len(site_block) > _TELEGRAM_MAX_LEN:
|
|
chunks.append(current.rstrip())
|
|
current = site_block
|
|
else:
|
|
current += site_block
|
|
|
|
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}")
|