- 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>
244 lines
8.7 KiB
Python
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()
|