/** * Telegram → resume PDF relay (Cloudflare Worker). * * WHY: Hugging Face Spaces block outbound traffic to api.telegram.org, so the HF * app can RECEIVE Telegram webhooks but can't SEND replies. This tiny Worker runs * on Cloudflare (which can reach Telegram), receives the webhook, calls the HF * /api/generate endpoint (which works publicly), and sends the PDF back to Telegram. * * It acks Telegram instantly and finishes the ~30–60s job via ctx.waitUntil so * Telegram never times out and retries. * * Deploy: paste into a Cloudflare Worker. Set these Variables/Secrets: * TELEGRAM_BOT_TOKEN — from @BotFather * ALLOWED_USER_IDS — comma-separated numeric Telegram user IDs (allowlist) * HF_API_URL — https://.hf.space * HF_API_TOKEN — your API_SECRET_TOKEN (matches the HF secret) * WEBHOOK_SECRET — optional; must match the setWebhook secret_token * Then point the Telegram webhook at the Worker URL (see relay/README.md). */ const HELP = "👋 Send me a *job link* (or paste the full job description) and I'll send back " + "your tailored, ATS-optimized resume PDF.\n\n" + "• Company/ATS links (Greenhouse, Lever, Ashby, Naukri) usually work directly.\n" + "• If I can't read a LinkedIn/Indeed link, copy the description text and send that.\n\n" + "Commands: /start, /help, /v1, /v2, /mode"; export default { async fetch(request, env, ctx) { if (request.method !== "POST") return new Response("ok"); // health/GET if (env.WEBHOOK_SECRET && request.headers.get("x-telegram-bot-api-secret-token") !== env.WEBHOOK_SECRET) { return new Response("forbidden", { status: 403 }); } let update; try { update = await request.json(); } catch { return new Response("ok"); } ctx.waitUntil(handle(update, env)); // finish in background; ack now return new Response("ok"); }, }; function parseAllowed(env) { // Robust: tolerate quotes, spaces, newlines around the comma-separated ids. return (env.ALLOWED_USER_IDS || "") .replace(/["']/g, "") .split(/[,\s]+/) .map((s) => s.trim()) .filter(Boolean) .map(Number) .filter((n) => !Number.isNaN(n)); } function tgUrl(env, method) { return `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/${method}`; } async function tg(env, method, payload) { try { await fetch(tgUrl(env, method), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } catch (_) { /* best-effort */ } } async function handle(update, env) { const msg = update.message || update.edited_message; if (!msg) return; const chatId = msg.chat && msg.chat.id; const userId = msg.from && msg.from.id; const text = (msg.text || "").trim(); if (chatId == null) return; // Diagnostic: /whoami always answers (before the allowlist) so you can compare // your real Telegram id against what the Worker actually sees in its env var. if (/^\/(whoami|id)\b/i.test(text)) { await tg(env, "sendMessage", { chat_id: chatId, text: `your id: ${userId}\n` + `ALLOWED_USER_IDS (raw): ${JSON.stringify(env.ALLOWED_USER_IDS ?? null)}\n` + `parsed: ${JSON.stringify(parseAllowed(env))}` }); return; } const allowed = parseAllowed(env); if (!allowed.length || !allowed.includes(Number(userId))) { await tg(env, "sendMessage", { chat_id: chatId, text: `⛔ Not authorized.\nyour id: ${userId}\n` + `ALLOWED_USER_IDS seen: ${JSON.stringify(env.ALLOWED_USER_IDS ?? null)}` }); return; } // ── Per-user version mode (Workers KV optional) ────────────────────────── const verKey = `ver:${userId}`; let userVer = 'v2'; if (env.USER_STATE) { try { userVer = (await env.USER_STATE.get(verKey)) || 'v2'; } catch (_) {} } if (/^\/v1\b/i.test(text)) { if (env.USER_STATE) { try { await env.USER_STATE.put(verKey, 'v1'); } catch (_) {} } await tg(env, "sendMessage", { chat_id: chatId, text: "✅ Mode set to V1 (Structured keyword placement)." }); return; } if (/^\/v2\b/i.test(text)) { if (env.USER_STATE) { try { await env.USER_STATE.put(verKey, 'v2'); } catch (_) {} } await tg(env, "sendMessage", { chat_id: chatId, text: "✅ Mode set to V2 (Natural AI sentence integration)." }); return; } if (/^\/mode\b/i.test(text)) { await tg(env, "sendMessage", { chat_id: chatId, text: `Current mode: ${userVer.toUpperCase()}` }); return; } if (!text || /^\/(start|help)\b/i.test(text)) { await tg(env, "sendMessage", { chat_id: chatId, text: HELP, parse_mode: "Markdown" }); return; } const urlMatch = text.match(/https?:\/\/\S+/); const form = new FormData(); if (urlMatch) { await tg(env, "sendMessage", { chat_id: chatId, text: "🔍 Reading the job & generating your resume… V2 runs several AI models, this can take 1–2 min." }); form.append("jd_url", urlMatch[0].replace(/[).,]+$/, "")); } else if (text.length >= 200) { await tg(env, "sendMessage", { chat_id: chatId, text: "⚙️ Generating your ATS resume… V2 runs several AI models, this can take 1–2 min." }); form.append("jd_text", text); } else { await tg(env, "sendMessage", { chat_id: chatId, text: "Send a job link, or paste the full job description (a paragraph or more)." }); return; } form.append("maximum_ats_mode", "1"); form.append("version", userVer); let res; try { const r = await fetch(`${env.HF_API_URL.replace(/\/$/, "")}/api/generate`, { method: "POST", headers: { "X-Api-Token": env.HF_API_TOKEN || "" }, body: form, }); res = await r.json(); } catch (e) { await tg(env, "sendMessage", { chat_id: chatId, text: "❌ Generation service is waking up or busy. Try again in a minute." }); return; } if (res.error === "jd_unreadable") { await tg(env, "sendMessage", { chat_id: chatId, text: "⚠️ I couldn't read that link (LinkedIn/Indeed block server access). Please copy the *job description text* and send it to me." , parse_mode: "Markdown" }); return; } if (res.error) { await tg(env, "sendMessage", { chat_id: chatId, text: `❌ ${res.detail || res.error}` }); return; } const cov = res.external_coverage_pct != null ? res.external_coverage_pct : (res.scores && res.scores.jd_match); if (res.pdf_b64) { await sendDoc(env, chatId, res.pdf_b64, "Saiteja_Tirunagari_Resume.pdf", "application/pdf", `✅ Tailored resume — ~${cov ?? "?"}% JD keyword coverage.`); } else if (res.tex_b64) { await sendDoc(env, chatId, res.tex_b64, "Saiteja_Tirunagari_Resume.tex", "application/x-tex", "⚠️ PDF compile failed — here's the .tex (compile at overleaf.com)."); } else { await tg(env, "sendMessage", { chat_id: chatId, text: "❌ Generation produced no file. Try again." }); } } function b64ToBytes(b64) { const bin = atob(b64); const bytes = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); return bytes; } async function sendDoc(env, chatId, b64, filename, mime, caption) { try { const fd = new FormData(); fd.append("chat_id", String(chatId)); fd.append("caption", caption.slice(0, 1024)); fd.append("document", new Blob([b64ToBytes(b64)], { type: mime }), filename); await fetch(tgUrl(env, "sendDocument"), { method: "POST", body: fd }); } catch (_) { await tg(env, "sendMessage", { chat_id: chatId, text: "❌ Couldn't upload the file to Telegram. Try again." }); } }