ibo/autofill_kontakt.py
timu 650d7f3394 ibo: containerize + Gitea-CI + dev/prod-Deploy + Zitadel-SSO + noVNC-Live-View
- Dockerfile/compose: Flask-App + selenium/standalone-chrome (noVNC), Healthcheck
- app.py: /healthz + eingebettete noVNC-Live-Ansicht (NOVNC_URL, lokal:7900 / Cluster:/novnc/)
- Helm-Chart infra/k8s/base/ibo: App + Selenium-Sidecar, 2 Ports, Ingress
  (/ + /novnc via stripPrefix), SSO-Middlewares aus dem CD-Repo
- Gitea-CI: version -> test -> build-push -> helm-validate -> deploy-dev (Write-back)
- .gitignore/.dockerignore (Dev-Samples raus)
- infra/RUNBOOK-ibo.md: Zitadel/DNS/Deploy/Prod-Promote

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 01:03:32 +02:00

244 lines
8.7 KiB
Python

#!/usr/bin/env python3
"""
Autofill-Script fuer das Kontaktformular (React / react-aria, z.B. ImmoScout24).
Fuellt Anrede, Vorname, Nachname, E-Mail, Telefon und Nachricht mit Mock-Daten.
Weil es ein React-Formular ist, werden echte Tastatur-Events via Selenium
gesendet (send_keys) - nur so uebernimmt React die Werte.
Die Feld-IDs sind dynamisch und aendern sich bei jedem Laden. Deshalb wird
ueber die stabilen name-Attribute selektiert. Felder kommen doppelt vor
(zwei gerenderte Varianten) -> es werden nur die SICHTBAREN gefuellt.
Setup:
pip install selenium
Benutzung:
python autofill_kontakt.py "https://www.immobilienscout24.de/..../kontakt"
# oder lokal (nur zum Ansehen, React laeuft dann nicht):
python autofill_kontakt.py
"""
import sys
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
# ---------------------------------------------------------------------------
# Werte kommen aus config.json (siehe config.py)
# ---------------------------------------------------------------------------
import config as _config
# Kopie, damit die App pro Lauf z.B. die E-Mail ueberschreiben kann,
# ohne die config.json zu veraendern.
MOCK = dict(_config.CFG["contact"])
def reload_mock():
"""Uebernimmt geaenderte config.json-Werte in MOCK."""
MOCK.clear()
MOCK.update(_config.reload()["contact"])
return MOCK
# ---------------------------------------------------------------------------
# Hilfsfunktionen (arbeiten nur auf sichtbaren Elementen)
# ---------------------------------------------------------------------------
def visible(driver, css):
"""Alle sichtbaren Elemente zu einem CSS-Selektor."""
els = driver.find_elements(By.CSS_SELECTOR, css)
return [e for e in els if e.is_displayed() and e.is_enabled()]
REACT_SET_JS = """
const el = arguments[0], val = arguments[1];
const proto = el.tagName === 'TEXTAREA'
? window.HTMLTextAreaElement.prototype
: window.HTMLInputElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
el.focus();
setter.call(el, ''); // erst leeren
el.dispatchEvent(new Event('input', {bubbles: true}));
setter.call(el, val); // dann Wert setzen
el.dispatchEvent(new Event('input', {bubbles: true})); // React onChange
el.dispatchEvent(new Event('change', {bubbles: true}));
el.dispatchEvent(new Event('blur', {bubbles: true})); // Validierung
"""
def type_into(driver, el, value, label=""):
"""React-sicher setzen: nativer value-Setter + input-Event (kein Tippen)."""
try:
driver.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
driver.execute_script(REACT_SET_JS, el, value)
print(f" [ok] {label or el.get_attribute('name')} = {value}")
except Exception as e:
print(f" [skip] {label}: {e.__class__.__name__}")
def fill_by_name(driver, name, value):
"""Fuellt NUR das erste sichtbare Feld dieses Namens (verhindert Dopplung)."""
els = visible(driver, f'input[name="{name}"], textarea[name="{name}"]')
if els:
type_into(driver, els[0], value, label=name)
else:
print(f" [skip] {name}: kein sichtbares Feld")
def choose_salutation(driver, value):
# react-aria Radios sind optisch versteckt (is_displayed==False) -> NICHT filtern.
els = driver.find_elements(
By.CSS_SELECTOR, f'input[name="salutation"][value="{value}"]')
for el in els:
try:
driver.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
# zugehoeriges Label klicken (react-aria reagiert darauf); sonst Input selbst
driver.execute_script("""
const el = arguments[0];
const lbl = el.closest('label')
|| (el.id && document.querySelector('label[for="'+el.id+'"]'));
(lbl || el).click();
el.checked = true;
el.dispatchEvent(new Event('click', {bubbles: true}));
el.dispatchEvent(new Event('change', {bubbles: true}));
""", el)
label = {"mr": "Herr", "mrs": "Frau"}.get(value, value)
print(f" [ok] Anrede = {label}")
return
except Exception as e:
print(f" [skip] Anrede: {e.__class__.__name__}")
print(" [skip] Anrede: kein Radio gefunden")
def fill_phone(driver, value):
"""Telefon: sichtbares tel-Feld (autocomplete='tel', ohne name)."""
for el in visible(driver, 'input[autocomplete="tel"]'):
type_into(driver, el, value, label="phone")
return
# Fallback: irgendein Feld mit inputmode text, das nicht schon belegt ist
print(" [info] kein dediziertes Telefon-Feld gefunden")
def click_cookie_banner(driver):
"""Cookie-Zustimmung wegklicken - auch in iframes (best effort)."""
texts = ["Alle akzeptieren", "Akzeptieren", "Alle zulassen",
"Zustimmen", "Einverstanden", "Accept all", "Accept"]
def try_click(ctx):
for txt in texts:
try:
btn = ctx.find_element(
By.XPATH, f"//button[contains(normalize-space(.), '{txt}')]")
if btn.is_displayed():
btn.click()
return True
except Exception:
pass
return False
if try_click(driver):
time.sleep(0.8)
return
# Consent liegt oft in einem iframe
for frame in driver.find_elements(By.TAG_NAME, "iframe"):
try:
driver.switch_to.frame(frame)
ok = try_click(driver)
except Exception:
ok = False
finally:
driver.switch_to.default_content()
if ok:
time.sleep(0.8)
return
def open_contact_form(driver):
"""Versucht, ueber einen Button das Kontaktformular zu oeffnen."""
labels = ["Nachricht", "Kontakt", "Anfrage", "Kontaktieren",
"Anbieter kontaktieren", "Nachricht senden", "Senden"]
for lbl in labels:
try:
btn = driver.find_element(
By.XPATH,
f"//button[contains(normalize-space(.), '{lbl}')] "
f"| //a[contains(normalize-space(.), '{lbl}')]")
if btn.is_displayed():
driver.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
btn.click()
print(f" Kontakt-Button geklickt: '{lbl}'")
time.sleep(2.0)
if visible(driver, 'input[name="email"]'):
return
except Exception:
pass
# ---------------------------------------------------------------------------
def main():
url = sys.argv[1] if len(sys.argv) > 1 else \
"file:///Users/timu/programming/momentrum/projekte/ibo/index2.html"
options = webdriver.ChromeOptions()
# options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options)
try:
print(f"Oeffne {url}")
driver.get(url)
time.sleep(2.0) # React-Bootstrap abwarten
# Cookie-Banner wegklicken (best effort, auch in iframes)
click_cookie_banner(driver)
# Kontaktformular ggf. erst per Button oeffnen
if not visible(driver, 'input[name="email"]'):
print("E-Mail-Feld noch nicht sichtbar -> versuche Kontakt-Button ...")
open_contact_form(driver)
# Aktiv auf das E-Mail-Feld warten (bis 25s)
print("Warte auf Kontaktformular ...")
deadline = 25
while deadline > 0 and not visible(driver, 'input[name="email"]'):
time.sleep(1.0)
deadline -= 1
found = visible(driver, 'input[name="email"]')
print(f"Sichtbare E-Mail-Felder gefunden: {len(found)}")
if not found:
print("!! Kein Kontaktformular sichtbar. Ist es evtl. in einem iframe")
print(" oder hinter einem weiteren Klick? Fenster bleibt offen -")
print(" oeffne das Formular manuell und starte das Script erneut,")
print(" ODER sag mir, wie man das Formular auf der Seite oeffnet.")
print("Anrede:")
choose_salutation(driver, MOCK["salutation"])
print("Textfelder:")
fill_by_name(driver, "firstName", MOCK["firstName"])
fill_by_name(driver, "lastName", MOCK["lastName"])
fill_by_name(driver, "email", MOCK["email"])
print("Telefon:")
fill_phone(driver, MOCK["phone"])
print("Nachricht:")
fill_by_name(driver, "message", MOCK["message"])
print("\nFertig. Formular ausgefuellt (nicht abgesendet).")
print("Fenster bleibt offen. Mit Strg+C beenden.")
while True:
time.sleep(3600)
except KeyboardInterrupt:
print("\nBeende ...")
driver.quit()
if __name__ == "__main__":
main()