v1.8.0 — Captcha-relay via Telegram (LLM-ready)
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
CaptchaSolverinterface plus two implementations:TelegramHumanSolver— production today.LLMVisionSolver— placeholder for v1.9+. Plugging in an LLM with
vision is a one-line swap inservice.py, nothing else changes.
PendingCaptchaSQLAlchemy model (newpending_captchatable,
created automatically bydb.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
CheckJCCaptchaFailedtyped 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 appNo 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.