feat: send matching jobs to telegram twice daily
telegram.ts
- Bot API wrapper: HTML escaping, 4096-char chunking, rate-limit
spacing, and --setup to discover the chat ID from the bot's inbox.
Reads TELEGRAM_BOT (or TELEGRAM_TOKEN) and TELEGRAM_CHAT_ID.
profile.ts
- matchesProfile() scores a title against cv/polo.md. Hard rejects for
Microsoft stack, Java/C++/SAP, aggregators (Jobgether, ALLPS),
non-software functions and life-science domains; then weighted
signals for the core stack, platform/DevOps, data, AI/ML, cloud and
seniority. Threshold is 4 so short bullseye titles ("Data Engineer",
"DevOps Engineer") pass, and forward-deployed is weighted to clear it
on its own.
daily.ts
- Runs the fetch, diffs jobs.csv, keeps only profile matches, groups by
area and pushes to Telegram. results/sent.txt records every link
actually sent, written only after a successful send, so a failure
retries instead of losing jobs and an ad is never sent twice.
- --dry-run previews, --resend --date= re-sends a past day.
- A failed fetch reports itself rather than going quiet.
Scheduled at 08:00 and 18:00 by a launchd agent
(com.rafapolo.linkme-daily). launchd runs missed jobs on wake, and the
digest is diff-based, so downtime delays delivery without losing jobs.
This commit is contained in:
+81
@@ -0,0 +1,81 @@
|
||||
// Telegram delivery for the daily digest.
|
||||
//
|
||||
// Setup:
|
||||
// 1. Message @BotFather on Telegram → /newbot → copy the token
|
||||
// 2. Put TELEGRAM_BOT=<token> in .env (TELEGRAM_TOKEN also accepted)
|
||||
// 3. Send your bot any message ("hi"), then run: bun run telegram.ts --setup
|
||||
// That prints your chat ID; put it in .env as TELEGRAM_CHAT_ID=<id>
|
||||
const env = Object.fromEntries(
|
||||
(await Bun.file(".env").text())
|
||||
.split("\n")
|
||||
.filter((l) => l.includes("=") && !l.trimStart().startsWith("#"))
|
||||
.map((l) => {
|
||||
const i = l.indexOf("=");
|
||||
return [l.slice(0, i).trim(), l.slice(i + 1).trim()];
|
||||
})
|
||||
);
|
||||
|
||||
export const TOKEN = env.TELEGRAM_BOT ?? env.TELEGRAM_TOKEN ?? "";
|
||||
export const CHAT_ID = env.TELEGRAM_CHAT_ID ?? env.TELEGRAM_CHAT ?? "";
|
||||
|
||||
const API = (m: string) => `https://api.telegram.org/bot${TOKEN}/${m}`;
|
||||
|
||||
/** Telegram hard-caps a message at 4096 chars; split on line boundaries. */
|
||||
function chunk(text: string, limit = 3900): string[] {
|
||||
const out: string[] = [];
|
||||
let buf = "";
|
||||
for (const line of text.split("\n")) {
|
||||
if (buf.length + line.length + 1 > limit) {
|
||||
out.push(buf);
|
||||
buf = "";
|
||||
}
|
||||
buf += (buf ? "\n" : "") + line;
|
||||
}
|
||||
if (buf) out.push(buf);
|
||||
return out;
|
||||
}
|
||||
|
||||
export function escapeHtml(s: string): string {
|
||||
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
export async function send(text: string): Promise<void> {
|
||||
if (!TOKEN || !CHAT_ID) {
|
||||
throw new Error("TELEGRAM_BOT / TELEGRAM_CHAT_ID missing from .env — see telegram.ts header");
|
||||
}
|
||||
for (const part of chunk(text)) {
|
||||
const res = await fetch(API("sendMessage"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
chat_id: CHAT_ID,
|
||||
text: part,
|
||||
parse_mode: "HTML",
|
||||
link_preview_options: { is_disabled: true },
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Telegram ${res.status}: ${await res.text()}`);
|
||||
await Bun.sleep(400); // stay under the ~1 msg/sec limit
|
||||
}
|
||||
}
|
||||
|
||||
// --setup: discover the chat ID from recent messages to the bot
|
||||
if (import.meta.main && process.argv.includes("--setup")) {
|
||||
if (!TOKEN) {
|
||||
console.error("Add TELEGRAM_BOT=<token> to .env first (get one from @BotFather).");
|
||||
process.exit(1);
|
||||
}
|
||||
const res = await fetch(API("getUpdates"));
|
||||
const data = (await res.json()) as any;
|
||||
const chats = new Map<string, string>();
|
||||
for (const u of data.result ?? []) {
|
||||
const c = u.message?.chat ?? u.channel_post?.chat;
|
||||
if (c) chats.set(String(c.id), c.username ?? c.title ?? c.first_name ?? "");
|
||||
}
|
||||
if (!chats.size) {
|
||||
console.log("No messages found. Send your bot a message in Telegram, then re-run this.");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("Add this to .env:\n");
|
||||
for (const [id, name] of chats) console.log(`TELEGRAM_CHAT_ID=${id} # ${name}`);
|
||||
}
|
||||
Reference in New Issue
Block a user