Spaces:
Sleeping
Sleeping
| /** | |
| * 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://<your-space>.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." }); | |
| } | |
| } | |