- booking_log.txt: midnight run results (cita booked at 15:15, 3347ms RTT) - fire_at_midnight.py: original midnight launch script - book_cita.sh / book_at_midnight.py: booking shell scripts - fetch_final.py / fetch_cita.sh: API fetch utilities - services_full.js, controllers_full.js, app_real2.js: frontend source - services_real.js, url.js: API endpoint references - tipos_doc.json: document type data from API - benchmark_results.json: server load profiling results
216 lines
7.5 KiB
Python
216 lines
7.5 KiB
Python
#!/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: <timestamp> {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)
|