JAA-ATS-Tool / relay /cloudflare-worker.js
saitejatirunagari's picture
feat: make V2 the default generation mode across all surfaces
58ed9c6
Raw
History Blame
7.66 kB
/**
* 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." });
}
}