Skip to content

feat(notify): add notify-telegram.mjs — Telegram notifications for new pipeline matches and interview invites#672

Open
Schlaflied wants to merge 3 commits into
santifer:mainfrom
Schlaflied:feat/notify-telegram
Open

feat(notify): add notify-telegram.mjs — Telegram notifications for new pipeline matches and interview invites#672
Schlaflied wants to merge 3 commits into
santifer:mainfrom
Schlaflied:feat/notify-telegram

Conversation

@Schlaflied
Copy link
Copy Markdown

@Schlaflied Schlaflied commented May 16, 2026

Closes #669

What this adds

notify-telegram.mjs sends Telegram messages when scan scripts find new matches or interview invites are detected. Designed to work standalone or piped from any scan script.

Modes

Mode Command
Send a message node notify-telegram.mjs --message "text"
Daily digest node notify-telegram.mjs --digest
Pipe from scan node scan.mjs --since 24h --json | node notify-telegram.mjs --stdin
Test setup node notify-telegram.mjs --auth
Preview add --dry-run to any mode

--digest output

Compiles two sections from local files (no API calls):

  • New pipeline entries today — from pipeline.md, matched by today's date header
  • Active interviews — from applications.md, rows with Interview status

--auth output

Tests the bot token and auto-detects chat_id from recent bot updates — no need to manually look up IDs.

--stdin / pipe mode

Reads a JSON payload from stdin matching the shape scan scripts will emit with a future --json flag:

{ "source": "scan.mjs", "items": [{ "url": "...", "title": "...", "company": "...", "bucket": "..." }] }

Falls back to plain text if JSON is not parseable.

Config

# config/profile.yml
telegram:
  bot_token: 123456789:ABC-xxx
  chat_id: "123456789"        # personal DM
  # or "-100xxxxxxxxxx" for a group/channel

Or via env vars: TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID

Scope

  • 1 new file: notify-telegram.mjs (271 lines)
  • 0 existing files modified
  • 0 new npm dependenciesjs-yaml (already in package.json) + native fetch

Test plan

  • --auth prints bot username and detects chat_id from recent updates
  • --message "hello" delivers a message to the configured chat
  • --dry-run --message "hello" prints preview without sending
  • --digest sends today's pipeline additions + active interview count
  • --stdin parses JSON payload and formats grouped message
  • --stdin falls back gracefully to plain text on non-JSON input
  • Missing bot_token / chat_id exits with a clear setup error

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a CLI tool to send Telegram notifications for the career-ops pipeline: token verification, single-message sending, stdin payload processing (grouped by bucket), and daily digest generation from pipeline/application data.
    • Supports profile-based configuration with environment-variable fallback and a dry-run mode to preview messages instead of sending.
  • Reliability

    • Safer HTML escaping for Telegram, improved handling and reporting of malformed inputs.

Review Change Stack

…peline events

Adds a zero-token notifier that sends Telegram messages when new job
matches are found or interview invites are detected. Works standalone
or piped from scan scripts.

Features:
- --message: send arbitrary text to your Telegram chat
- --digest: daily summary of today's pipeline additions + active interviews
- --stdin: consume JSON payload piped from scan scripts (--json flag)
- --auth: test bot token + auto-detect chat_id from recent updates
- --dry-run: preview message without sending
- HTML formatting (bold, links) via Telegram's parse_mode=HTML
- Config via config/profile.yml under telegram: key (bot_token, chat_id)
  or TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID env vars

Closes santifer#669
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

📝 Walkthrough

Walkthrough

New CLI script notify-telegram.mjs sends Telegram notifications for the career-ops pipeline. Supports --auth, --message, --stdin, and --digest. Loads config from config/profile.yml with environment fallbacks, HTML-escapes content, and supports --dry-run.

Changes

Telegram Notification Script

Layer / File(s) Summary
Configuration and CLI setup
notify-telegram.mjs
Executable entry point, CLI docs, pipeline/config path constants, and flag parsing. loadConfig() reads config/profile.yml with TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID fallbacks and validates presence when not in --auth mode.
HTTP helpers and URL sanitization
notify-telegram.mjs
fetchWithTimeout() enforces request timeouts using AbortController. sanitizeUrl() validates and normalizes URLs for safe Telegram HTML links.
Telegram API integration
notify-telegram.mjs
telegramFetch() posts JSON to Telegram Bot API and validates data.ok. sendMessage() sends HTML-mode messages, disables web page previews, and supports --dry-run to print instead of sending.
Auth discovery
notify-telegram.mjs
runAuth() verifies bot token via getMe, calls getUpdates, extracts unique chats from message.chat and channel_post.chat, and prints discovered chat IDs for configuration.
Daily digest assembly
notify-telegram.mjs
buildDigest() parses data/pipeline.md for today’s pipeline entries and data/applications.md for active interview rows, assembling a bounded HTML digest or returning null if empty.
Stdin payload formatting
notify-telegram.mjs
readStdin() reads UTF-8 stdin. formatPipelinePayload() parses JSON input, groups items by bucket, and renders per-bucket HTML with per-entry company/link formatting; falls back to truncated raw payload if parsing/formatting fails. escapeHtml() escapes &, <, and > for safe HTML.
Main orchestration and error handling
notify-telegram.mjs
main() routes execution for --auth, --message, --stdin, and --digest, handles "no input"/"nothing to report" cases, invokes sendMessage(), and prints success unless --dry-run is set. Top-level promise rejection handler logs errors and exits with status 1.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a new Telegram notification script (notify-telegram.mjs) for pipeline and interview notifications.
Linked Issues check ✅ Passed The pull request fully implements all coding objectives from issue #669: notify-telegram.mjs with --auth, --message, --digest, --stdin modes, Telegram Bot API integration, config loading, and error handling.
Out of Scope Changes check ✅ Passed All changes are within scope—only notify-telegram.mjs (303 new lines) was added; no unrelated files or features beyond the Telegram notifier requirements were modified.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@notify-telegram.mjs`:
- Around line 157-158: The HTML injection uses i.url (and similar e.url)
directly in href which can break markup if the URL contains quotes or malicious
payloads; update the code in notify-telegram.mjs where newItems and entries are
rendered (the lines that push `• <a href="${i.url}">...` and the other
occurrences around those blocks) to validate/sanitize URLs before interpolating:
parse and allow only safe schemes (http/https), normalize/encode the URL (e.g.,
via new URL(...) and encodeURI or similar) and escape any remaining HTML-special
characters (including quotes) before inserting into href, and reject or omit
invalid/unsafe URLs. Ensure you apply the same fix at the other occurrences
noted (around lines 208 and 220-224).
- Around line 77-82: The three fetch calls that use TELEGRAM_BASE (the POST to
`${TELEGRAM_BASE}/bot${botToken}/${method}`, the GET to
`${TELEGRAM_BASE}/bot${botToken}/getMe`, and the GET to
`${TELEGRAM_BASE}/bot${botToken}/getUpdates?limit=10`) must use AbortController
to enforce a timeout (e.g., 10_000 ms) so the CLI/scheduled jobs don't hang;
create an AbortController, pass controller.signal to fetch, set a setTimeout to
call controller.abort() after 10000ms, clear the timeout after the fetch
completes, and handle the abort/timeout error path when awaiting res.json() for
variables like res, data, and updates.
- Around line 51-58: The script currently exits when PROFILE_PATH is missing,
making the environment-variable fallback unreachable; update the logic around
PROFILE_PATH/loadYaml/readFileSync so the script does not process.exit(1) on a
missing profile file — instead log a warning and treat profile as an empty
object (or only attempt readFileSync when existsSync is true), then compute
botToken and chatId from profile.telegram or process.env.TELEGRAM_BOT_TOKEN /
TELEGRAM_CHAT_ID; reference the PROFILE_PATH check, loadYaml/readFileSync usage
and the botToken/chatId assignments to locate and adjust the code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2095a869-91f8-4097-86a7-b264fe865ceb

📥 Commits

Reviewing files that changed from the base of the PR and between 5d1f3a3 and 3c8dd6d.

📒 Files selected for processing (1)
  • notify-telegram.mjs

Comment thread notify-telegram.mjs Outdated
Comment thread notify-telegram.mjs Outdated
Comment thread notify-telegram.mjs Outdated
- loadConfig: don't exit on missing profile.yml — warn and fall back to
  env vars (TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID) so env-only setups work
- fetchWithTimeout: add AbortController with 10s timeout to all Telegram
  API calls to prevent CLI/scheduled jobs from hanging indefinitely
- sanitizeUrl: validate href URLs allow only http/https and encode special
  chars before interpolating into HTML anchor tags; omit link on invalid URL
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@notify-telegram.mjs`:
- Around line 112-119: The script currently calls loadConfig() before checking
DRY_RUN, causing dry-run to fail on missing credentials; change the order so the
DRY_RUN check runs first and returns the preview output immediately (bypassing
loadConfig and any config validation/credential checks), i.e., move the DRY_RUN
branch above the loadConfig() invocation and only call loadConfig() when not in
dry-run mode.
- Around line 245-248: The catch block currently returns raw payload on JSON
parse failure while elsewhere messages are sent with parse_mode: 'HTML', which
risks breaking Telegram parsing; update the fallback in the catch to HTML-escape
the payload (replace &, <, >, and optionally " and ') before truncating to 4000
chars and returning it so the returned string is safe for use with parse_mode:
'HTML' (implement or reuse an escapeHtml helper and use it in the catch where
payload is sliced).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e5cef34e-aa17-4931-ab33-490ae632521c

📥 Commits

Reviewing files that changed from the base of the PR and between 3c8dd6d and 19070d0.

📒 Files selected for processing (1)
  • notify-telegram.mjs

Comment thread notify-telegram.mjs Outdated
Comment thread notify-telegram.mjs
- sendMessage: move DRY_RUN check before loadConfig() so dry-run mode
  never requires credentials — loadConfig only called when actually sending
- formatPipelinePayload catch: HTML-escape raw payload before truncating
  to 4000 chars so the plain-text fallback is safe for parse_mode: HTML
@Schlaflied
Copy link
Copy Markdown
Author

@coderabbitai resolve

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@notify-telegram.mjs`:
- Around line 46-48: The CLI currently treats a bare "--message" flag without a
following value as if no message was provided; update the parsing around
msgArgIdx and MESSAGE to validate that when msgArgIdx !== -1 the next argv
element exists and is not another flag (e.g., not starting with '--'), and if
missing or invalid immediately print a clear CLI error and exit non-zero; apply
the same validation to the other identical message-parsing site noted in the
file (the block around the later message parsing).
- Around line 120-124: The CLI raw message passed as MESSAGE is sent with
parse_mode 'HTML' in telegramFetch('sendMessage') without escaping, causing
Telegram API 400 errors for '<', '>', '&'; update the code path that uses the
MESSAGE value (the branch that supplies text to telegramFetch/sendMessage) to
call the existing escapeHtml(MESSAGE) before passing it as text so the payload
uses escaped HTML; ensure you reference the escapeHtml function and replace the
raw MESSAGE variable with escapeHtml(MESSAGE) wherever the --message branch
builds the sendMessage payload.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2271f685-3d55-445f-af72-8ed9f6fc63bc

📥 Commits

Reviewing files that changed from the base of the PR and between 19070d0 and e00e605.

📒 Files selected for processing (1)
  • notify-telegram.mjs

Comment thread notify-telegram.mjs
Comment thread notify-telegram.mjs
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

✅ Actions performed

Comments resolved. Approval is disabled; enable reviews.request_changes_workflow to allow explicit top-level @coderabbitai resolve or @coderabbitai approve commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: notify-telegram.mjs — Telegram bot notifications for new interview invites and pipeline matches

1 participant