#!/usr/bin/env python3 """ Midnight cita booking for Portugal Tardes (cola I), Oct 19, 2026. Hardened against: - Load-induced TLS/response delays (20s timeout, pre-established TLS) - Duplicate bookings (sequential, availability check before booking, tracking ID) - Unknown API responses (explicit success detection, graceful failure) - Bot competition (sweeps all 9 slots if first is taken) Usage: Execute at 23:59:56 with `python3 /tmp/fire_at_midnight.py` """ import http.client, ssl, json, time, sys, uuid from datetime import datetime # ── Configuration ────────────────────────────────────────── HOST = "webservices.alicante.es" PORT = 443 API_PATH = "/WsCitaPreviaPublic-RESTWebService-context-root/rest/1" CITA_PATH = API_PATH + "/ScCitaVo1" CT = "application/vnd.oracle.adf.action+json" SERVICIO = "S" COLA = "I" FECHA = "19-10-2026" DOC = "X2491454W" NOMBRE = "Humberto" APE1 = "Normey" APE2 = "Gomez" TIPO = "2" # NIE TEL = "687915599" EMAIL = "hj@normey.net" IMPUESTOS = "N" COMENTARIO = "Solicitud de Alta en el Padrón" IP = "0.0.0.0" # Unique tracking reference to identify/reconcile duplicates TRACKING_ID = f"HNG-{uuid.uuid4().hex[:8]}" # All 9 known slots (verified via getHorasCitas) ALL_HORAS = ["14:30","14:45","15:00","15:15","15:30","15:45","16:00","16:15","16:30"] # Priority order: 16:00 first (user's request), then 14:30, 14:45, etc. PRIORITY = ["16:00","14:30","14:45","15:00","15:15","15:30","15:45","16:15","16:30"] # ── Target: Oct 4, 2026 00:00:00 CEST ───────────────────── target_ts = datetime(2026, 10, 4, 0, 0, 0).timestamp() # ── Response constants (from controllers.js line 430) ───── R_SUCCESS = "Cita insertada" R_DATE_LIMIT = "Fecha posterior al limite permitido" R_TIME_INVALID = "Hora no permitida" def log(msg): ts = datetime.now().strftime('%H:%M:%S.%f')[:-3] print(f"[{ts}] {msg}", flush=True) def build_booking_body(hora): motivo = f"{datetime.now().strftime('%H:%M:%S')} {COMENTARIO} [{TRACKING_ID}]" return json.dumps({ "name": "insertarCitaPrevia", "parameters": [ {"servicio": SERVICIO}, {"cola": COLA}, {"fecha": FECHA}, {"hora": hora}, {"documento": DOC}, {"ip": IP}, {"nombre": NOMBRE}, {"apellido1": APE1}, {"apellido2": APE2}, {"tipo": TIPO}, {"telefono": TEL}, {"email": EMAIL}, {"impuestos": IMPUESTOS}, {"motivo": motivo}, {"procedencia": "NULO"}, ] }, separators=(',', ':')) def build_check_body(): """getHorasCitas to check current availability""" return json.dumps({ "name": "getHorasCitas", "parameters": [ {"servicio": SERVICIO}, {"cola": COLA}, {"fecha": FECHA}, ] }, separators=(',', ':')) def make_request(conn, body, timeout_note=""): """Send POST and return (result_string, raw_response)""" headers = { "Content-Type": CT, "Origin": "https://citaprevia.alicante.es", "Referer": "https://citaprevia.alicante.es/CitaPrevia/", } try: conn.request("POST", CITA_PATH, body=body, headers=headers) resp = conn.getresponse() raw = resp.read().decode() try: result = json.loads(raw).get('result', '') except: result = raw[:300] if raw else "(empty)" return result, raw, resp.status except Exception as e: return f"EXCEPTION: {e}", "", 0 def pre_connect(): """Establish TLS connection (done before midnight to save handshake time)""" try: ctx = ssl.create_default_context() conn = http.client.HTTPSConnection(HOST, PORT, context=ctx, timeout=20) conn.connect() return conn, ctx except Exception as e: log(f"Pre-connect failed: {e}") return None, None # ── Main ─────────────────────────────────────────────────── now = datetime.now() log(f"Bot starting Tracking ID: {TRACKING_ID}") log(f"Target midnight: {datetime.fromtimestamp(target_ts)}") log(f"Time until midnight: {target_ts - now.timestamp():.1f}s") log(f"Pre-connecting TLS...") conn, ctx = pre_connect() if conn: log("TLS pre-established ✓") else: log("Pre-connect failed — will reconnect at midnight") log("Busy-waiting until midnight...") # ── Busy-wait until exactly 00:00:00 ────────────────────── while time.time() < target_ts: pass log("*** MIDNIGHT — Firing ***") # ── Phase 1: Check available slots (getHorasCitas) ──────── log("Phase 1: Checking available slots via getHorasCitas...") if not conn: conn, ctx = pre_connect() check_body = build_check_body() result, raw, status = make_request(conn, check_body) log(f" Availability check: {result[:200]}") # Parse available slots available_slots = [] try: resp_json = json.loads(raw) slots_raw = json.loads(resp_json.get('result', '[]')) available_slots = [s['hora'] for s in slots_raw] log(f" Available slots: {len(available_slots)} — {available_slots}") except Exception as e: log(f" Could not parse availability: {e}") log(f" Will attempt all known slots anyway") available_slots = ALL_HORAS # fallback # ── Phase 2: Book in priority order ─────────────────────── # Filter priority list to only available slots to_try = [h for h in PRIORITY if h in available_slots] or PRIORITY log(f"Phase 2: Attempting booking in priority order: {to_try}") booked = False for hora in to_try: body = build_booking_body(hora) log(f" → Booking: {FECHA} at {hora}") result, raw, status = make_request(conn, body) log(f" ← Response ({status}): {result[:200]}") if result == R_SUCCESS: log("") log("✅ CITA BOOKED SUCCESSFULLY!") log(f" Fecha: {FECHA}") log(f" Hora: {hora}") log(f" Servicio: {SERVICIO} (SAC/SAIC)") log(f" Cola: {COLA} (C/PORTUGAL 17 TARDE - PADRÓN)") log(f" Nombre: {NOMBRE} {APE1} {APE2}") log(f" NIE: {DOC}") log(f" Tracking: {TRACKING_ID}") log(f" Motivo: {COMENTARIO} [{TRACKING_ID}]") log("") log(f" To verify: I'll re-check getHorasCitas — slot {hora} should be gone") log(f" To print: https://citaprevia.alicante.es/CitaPrevia/#/verCita/{DOC}/{SERVICIO}{COLA}{DOC}/OLD") booked = True break elif result == R_DATE_LIMIT: log(f" → Date limit error (unexpected after midnight). Retrying...") time.sleep(0.05) # brief pause, then retry same slot elif result == R_TIME_INVALID: log(f" → Slot {hora} already taken. Trying next...") else: log(f" → Unexpected response: {result[:100]} — skipping to next slot") # Brief pause between attempts (avoid hammering, but keep it fast) time.sleep(0.02) if not booked: log("") log("❌ ALL ATTEMPTS FAILED") log(f" Tried {len(to_try)} slots for {FECHA}") log(f" Tracking ID: {TRACKING_ID}") log(f" The {len(available_slots)} slots were likely grabbed by bots in <1s") log(f" Check back tomorrow — new slots open daily for the next working day") sys.exit(0 if booked else 1)