Skip to content

v1.8.0 — Captcha-relay via Telegram (LLM-ready)

Choose a tag to compare

@Gonzalez8 Gonzalez8 released this 22 May 16:44
· 45 commits to main since this release

CheckJC added a captcha. We bypass it.

Between v1.7.x deploys and v1.8.0, CheckJC introduced a new gate after
login: /portal/employee/verification, with a 6-digit distorted
captcha and an on-screen keypad that reshuffles after every click.
None of the previous code paths could pass it, so 100% of automatic
fichajes were broken.

The key discovery

Each keypad button carries a stable per-session data-value letter
(e.g. N, L, X...). The letter ↔ digit mapping is fixed for the
entire session
. Only the physical positions of the buttons reshuffle
after each click.

Validated end-to-end twice against the real CheckJC: read the
keypad once at the start, OCR the 10 clean digit PNGs (Tesseract with
psm=10), and then drive every click by letter — re-reading the DOM
positions between clicks since the buttons move, but never the
letter↔digit map.

For the distorted captcha itself we go through a human-in-the-loop:
the scheduler captures the image, ships it via sendPhoto to the
user's Telegram chat, and waits for the 6-digit reply.

What ships

  • CaptchaSolver interface plus two implementations:
    • TelegramHumanSolver — production today.
    • LLMVisionSolver — placeholder for v1.9+. Plugging in an LLM with
      vision is a one-line swap in service.py, nothing else changes.
  • PendingCaptcha SQLAlchemy model (new pending_captcha table,
    created automatically by db.create_all, no manual migration).
  • keypad_reader.py — Tesseract OCR for the keypad's 10 clean digit
    PNGs (psm=10, digit whitelist).
  • CheckJCClient._solve_verification — orchestrates: capture
    captcha → capture keypad → OCR mapping → solver → click loop with
    per-click DOM re-read → submit → verify landing on /portal/employee.
    One retry on wrong reply before giving up.
  • Bot listener: intercepts 6-digit replies before normal command
    handling and writes them back to the pending row; periodic sweeper
    expires stale rows after their TTL.
  • New CheckJCCaptchaFailed typed error with its own Telegram
    formatting (🧩) so users see something actionable.

What you'll see in production

09:00:00  Scheduler picks up the fichaje
09:00:05  Login OK → lands on /verification
09:00:06  Bot sends captcha image to user's Telegram with caption
09:00:30  User replies "578599"
09:00:31  Bot writes ANSWERED to the pending row
09:00:32  Scheduler exits the polling, translates digits → letters,
          clicks 6 letters (re-reading DOM each click), submits
09:00:35  Lands on /portal/employee, clicks btn-check
09:00:36  Telegram: "🟢 Check in completed successfully"

Captcha-relay TTL is 5 minutes. If the user is slow, we send
⌛ Tiempo agotado and skip the fichaje — they need to do it manually
in checkjc.com.

Image

ghcr.io/gonzalez8/checktime:1.8.0 / :1.8 / :latest

Image is slightly larger (~10 MB) because it now ships
tesseract-ocr. Python deps gain pytesseract and Pillow.

Upgrade

sed -i 's/^CHECK_TIME_VERSION=.*/CHECK_TIME_VERSION=1.8.0/' stack.env
docker compose pull app
docker compose up -d app

No new mandatory env vars. Database migration is automatic on first
boot (new table only).

Design rationale and the failure-mode notes for CheckJCCaptchaFailed
live in docs/migrations/captcha-relay.md.