/** * notify.mjs — Telegram notifications * Sends messages directly via Telegram Bot API */ import { TELEGRAM_API_BASE, NOTIFY_RATE_LIMIT_MS } from './constants.mjs'; let lastSentAt = 0; export async function sendTelegram(settings, message) { const { bot_token, telegram_user_id } = settings.notifications; if (!bot_token || !telegram_user_id) { console.log(`[notify] No Telegram config — would send: ${message.substring(0, 80)}`); return; } // Rate limit to avoid Telegram API throttling const now = Date.now(); const elapsed = now - lastSentAt; if (elapsed < NOTIFY_RATE_LIMIT_MS) { await new Promise(r => setTimeout(r, NOTIFY_RATE_LIMIT_MS - elapsed)); } const url = `${TELEGRAM_API_BASE}${bot_token}/sendMessage`; try { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: telegram_user_id, text: message, parse_mode: 'Markdown', }), }); lastSentAt = Date.now(); const data = await res.json(); if (!data.ok) console.error('[notify] Telegram error:', data.description); } catch (e) { console.error('[notify] Failed to send Telegram message:', e.message); } } export function formatSearchSummary(added, skipped, platforms) { if (added === 0) return `šŸ” *Job Search Complete*\nNo new jobs found this run.`; return `šŸ” *Job Search Complete*\n${added} new job${added !== 1 ? 's' : ''} added to queue (${skipped} already seen)\nPlatforms: ${platforms.join(', ')}`; } export function formatApplySummary(results) { const { submitted, failed, needs_answer, total, skipped_recruiter, skipped_external, skipped_no_easy_apply } = results; const lines = [ `āœ… *Apply Run Complete*`, `Applied: ${submitted} | Failed: ${failed} | Needs answer: ${needs_answer}`, `Skipped: ${skipped_recruiter} recruiter-only | ${skipped_external} external ATS | ${skipped_no_easy_apply} no Easy Apply`, `Total processed: ${total}`, ]; if (needs_answer > 0) lines.push(`\nšŸ’¬ Check messages — I sent questions that need your answers`); if (skipped_external > 0) lines.push(`\nšŸ”œ ${skipped_external} external ATS jobs saved for when Greenhouse/Lever support lands`); return lines.join('\n'); } export function formatUnknownQuestion(job, question) { return `ā“ *Unknown question while applying*\n\n*Job:* ${job.title} @ ${job.company}\n*Question:* "${question}"\n\nWhat should I answer? (Reply and I'll save it for all future runs)`; }