Fix Degewo auto-apply (Wohnungshelden iframe), update dependencies, cleanup for production
- Fix Degewo handler to work with Wohnungshelden iframe portal - Update playwright to >=1.57.0 - Add proper form field selectors for Wohnungshelden - Fix success status bug (was marking failed submissions as success) - Clean up .env.example (remove real credentials) - Update README with housing company support table - Add BOTFATHER_COMMANDS.txt for easy bot setup - Add copilot-instructions.md for development context
This commit is contained in:
parent
bf9d7f1371
commit
ce66fc1933
12 changed files with 854 additions and 3599 deletions
32
.env.example
32
.env.example
|
|
@ -3,9 +3,31 @@
|
||||||
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
||||||
TELEGRAM_CHAT_ID=your_chat_id_here
|
TELEGRAM_CHAT_ID=your_chat_id_here
|
||||||
|
|
||||||
# inberlinwohnen.de Login
|
# inberlinwohnen.de Login (optional - for personalized filtered results)
|
||||||
INBERLIN_EMAIL=aron@petau.net
|
INBERLIN_EMAIL=your_email@example.com
|
||||||
INBERLIN_PASSWORD=BvA5n0iKmGV1
|
INBERLIN_PASSWORD=your_password_here
|
||||||
|
|
||||||
# Check interval in seconds (default: 600 = 10 minutes)
|
# Check interval in seconds (default: 300 = 5 minutes)
|
||||||
CHECK_INTERVAL=600
|
CHECK_INTERVAL=300
|
||||||
|
|
||||||
|
# Form Data for Autopilot Applications
|
||||||
|
FORM_ANREDE=Herr
|
||||||
|
FORM_VORNAME=Max
|
||||||
|
FORM_NACHNAME=Mustermann
|
||||||
|
FORM_EMAIL=max@example.com
|
||||||
|
FORM_PHONE=030123456789
|
||||||
|
FORM_STRASSE=Musterstraße
|
||||||
|
FORM_HAUSNUMMER=1
|
||||||
|
FORM_PLZ=10115
|
||||||
|
FORM_ORT=Berlin
|
||||||
|
FORM_PERSONS=2
|
||||||
|
FORM_CHILDREN=0
|
||||||
|
FORM_INCOME=2500
|
||||||
|
|
||||||
|
# WGcompany.de Search Filters (optional)
|
||||||
|
WGCOMPANY_ENABLED=true
|
||||||
|
WGCOMPANY_MIN_SIZE=
|
||||||
|
WGCOMPANY_MAX_SIZE=
|
||||||
|
WGCOMPANY_MIN_PRICE=
|
||||||
|
WGCOMPANY_MAX_PRICE=
|
||||||
|
WGCOMPANY_BEZIRK=0
|
||||||
|
|
|
||||||
81
.github/copilot-instructions.md
vendored
Normal file
81
.github/copilot-instructions.md
vendored
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
# Copilot Instructions for inberlin-monitor
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
A Python-based apartment monitoring bot for Berlin's public housing portal (inberlinwohnen.de) and WG rooms (wgcompany.de). Monitors listings from 6 housing companies (HOWOGE, Gewobag, Degewo, Gesobau, Stadt und Land, WBM) plus WGcompany, and sends Telegram notifications with optional auto-application via Playwright browser automation.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**Single-file monolith** (`monitor.py`, ~1600 lines) with five main classes:
|
||||||
|
- `InBerlinMonitor` - Core scraping/monitoring loop for inberlinwohnen.de, login handling, listing detection
|
||||||
|
- `WGCompanyMonitor` - Monitors wgcompany.de WG rooms with configurable search filters
|
||||||
|
- `ApplicationHandler` - Company-specific form automation (each `_apply_*` method handles one housing company)
|
||||||
|
- `TelegramBot` - Command handling via long-polling in a daemon thread
|
||||||
|
- Main loop runs synchronous with `asyncio.get_event_loop().run_until_complete()` for Playwright calls
|
||||||
|
|
||||||
|
**Data flow**: Fetch listings → Compare with `listings.json` / `wgcompany_listings.json` → Detect new → Log to CSV → Auto-apply if autopilot enabled (inberlin only) → Save to `applications.json` → Send Telegram notification
|
||||||
|
|
||||||
|
## Key Patterns
|
||||||
|
|
||||||
|
### Company-specific handlers
|
||||||
|
Each housing company has a dedicated `_apply_{company}()` method in `ApplicationHandler`. When adding support for a new company:
|
||||||
|
1. Add detection in `_detect_company()` (line ~350)
|
||||||
|
2. Add handler call in `apply()` switch (line ~330)
|
||||||
|
3. Implement `_apply_newcompany()` following existing patterns (cookie dismiss → find button → fill form → submit → screenshot)
|
||||||
|
|
||||||
|
### Listing identification
|
||||||
|
Listings are hashed by `md5(key_fields)[:12]` to generate stable IDs:
|
||||||
|
- InBerlin: `md5(rooms+size+price+address)`
|
||||||
|
- WGcompany: `md5(link+price+size)`
|
||||||
|
|
||||||
|
### State management
|
||||||
|
- `state.json` - Runtime state (autopilot toggle)
|
||||||
|
- `listings.json` - Previously seen inberlinwohnen listings
|
||||||
|
- `wgcompany_listings.json` - Previously seen WGcompany listings
|
||||||
|
- `applications.json` - Application history with success/failure status
|
||||||
|
- `listing_times.csv` / `wgcompany_times.csv` - Time-series data for pattern analysis
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Run locally
|
||||||
|
```bash
|
||||||
|
# Install deps (requires Playwright)
|
||||||
|
pip install -r requirements.txt
|
||||||
|
playwright install chromium
|
||||||
|
|
||||||
|
# Set env vars and run
|
||||||
|
export TELEGRAM_BOT_TOKEN=... TELEGRAM_CHAT_ID=...
|
||||||
|
python monitor.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker (production)
|
||||||
|
```bash
|
||||||
|
cp .env.example .env # Configure credentials
|
||||||
|
docker compose up -d
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
- Screenshots saved to `data/` on application failures (`*_nobtn_*.png`)
|
||||||
|
- HTML saved to `data/debug_page.html` (inberlin) and `data/wgcompany_debug.html`
|
||||||
|
- Full logs in `data/monitor.log`
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Required: `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`
|
||||||
|
InBerlin login: `INBERLIN_EMAIL`, `INBERLIN_PASSWORD`
|
||||||
|
Form data: `FORM_ANREDE`, `FORM_VORNAME`, `FORM_NACHNAME`, `FORM_EMAIL`, `FORM_PHONE`, `FORM_STRASSE`, `FORM_HAUSNUMMER`, `FORM_PLZ`, `FORM_ORT`, `FORM_PERSONS`, `FORM_CHILDREN`, `FORM_INCOME`
|
||||||
|
WGcompany: `WGCOMPANY_ENABLED`, `WGCOMPANY_MIN_SIZE`, `WGCOMPANY_MAX_SIZE`, `WGCOMPANY_MIN_PRICE`, `WGCOMPANY_MAX_PRICE`, `WGCOMPANY_BEZIRK`
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Fix a broken company handler
|
||||||
|
Check `data/*_nobtn_*.png` screenshots and `data/debug_page.html` to see actual page structure. Update selectors in the corresponding `_apply_{company}()` method.
|
||||||
|
|
||||||
|
### Add Telegram command
|
||||||
|
1. Add case in `TelegramBot._handle_update()` (line ~95)
|
||||||
|
2. Implement `_handle_{command}_command()` method
|
||||||
|
|
||||||
|
### Modify listing extraction
|
||||||
|
- InBerlin: Update regex patterns in `InBerlinMonitor.fetch_listings()`. Test against `data/debug_page.html`.
|
||||||
|
- WGcompany: Update parsing in `WGCompanyMonitor.fetch_listings()`. Test against `data/wgcompany_debug.html`.
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -4,10 +4,12 @@ __pycache__/
|
||||||
*.class
|
*.class
|
||||||
*.so
|
*.so
|
||||||
.Python
|
.Python
|
||||||
|
.venv
|
||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
ENV/
|
ENV/
|
||||||
env/
|
env/
|
||||||
|
.python-version
|
||||||
|
|
||||||
# Data
|
# Data
|
||||||
data/
|
data/
|
||||||
|
|
@ -24,3 +26,8 @@ data/
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Debug files (these should be in data/ but just in case)
|
||||||
|
debug_page.html
|
||||||
|
*.html
|
||||||
|
!README.md
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
3.11.8
|
3.13
|
||||||
|
|
|
||||||
6
BOTFATHER_COMMANDS.txt
Normal file
6
BOTFATHER_COMMANDS.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
Copy this to BotFather when setting commands with /setcommands:
|
||||||
|
|
||||||
|
autopilot - Toggle automatic applications (on/off)
|
||||||
|
status - Show current status and stats
|
||||||
|
plot - Show weekly listing patterns
|
||||||
|
help - Show available commands
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
FROM mcr.microsoft.com/playwright/python:v1.56.0-jammy
|
FROM mcr.microsoft.com/playwright/python:v1.57.0-jammy
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
|
||||||
48
README.md
48
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# inberlin-monitor
|
# inberlin-monitor
|
||||||
|
|
||||||
Monitors [inberlinwohnen.de](https://www.inberlinwohnen.de/wohnungsfinder/) for new apartment listings and sends Telegram notifications. Supports automatic application submission via autopilot mode.
|
Monitors [inberlinwohnen.de](https://www.inberlinwohnen.de/wohnungsfinder/) and [wgcompany.de](http://www.wgcompany.de) for new apartment/WG listings and sends Telegram notifications. Supports automatic application submission via autopilot mode.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|
@ -9,10 +9,22 @@ Monitors [inberlinwohnen.de](https://www.inberlinwohnen.de/wohnungsfinder/) for
|
||||||
- 📱 Sends Telegram notifications for new listings with clickable links
|
- 📱 Sends Telegram notifications for new listings with clickable links
|
||||||
- 🤖 **Autopilot mode**: Automatically applies to new listings
|
- 🤖 **Autopilot mode**: Automatically applies to new listings
|
||||||
- 📊 **/plot command**: Visualize when listings appear throughout the week
|
- 📊 **/plot command**: Visualize when listings appear throughout the week
|
||||||
- 🏢 Supports multiple housing companies: HOWOGE, Gewobag, Degewo, Gesobau, Stadt und Land, WBM
|
- 🏢 Supports 6 housing companies: HOWOGE, Gewobag, Degewo, Gesobau, Stadt und Land, WBM
|
||||||
|
- 🏠 **WGcompany.de**: Also monitors WG room listings with configurable search filters
|
||||||
- 💾 Persists state to detect only truly new listings
|
- 💾 Persists state to detect only truly new listings
|
||||||
- 📈 Logs listing times for pattern analysis
|
- 📈 Logs listing times for pattern analysis
|
||||||
|
|
||||||
|
## Supported Housing Companies
|
||||||
|
|
||||||
|
| Company | Auto-Apply | Notes |
|
||||||
|
|---------|------------|-------|
|
||||||
|
| HOWOGE | ✅ | Direct form submission |
|
||||||
|
| Gewobag | ✅ | Direct form submission |
|
||||||
|
| Degewo | ✅ | Via Wohnungshelden portal |
|
||||||
|
| Gesobau | ✅ | Direct form submission |
|
||||||
|
| Stadt und Land | ✅ | Direct form submission |
|
||||||
|
| WBM | ✅ | Direct form submission |
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
### 1. Create Telegram Bot
|
### 1. Create Telegram Bot
|
||||||
|
|
@ -80,13 +92,28 @@ cat data/monitor.log
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|----------|-------------|---------|
|
|----------|-------------|---------|
|
||||||
| `FORM_ANREDE` | Salutation (Herr/Frau) | Herr |
|
| `FORM_ANREDE` | Salutation (Herr/Frau) | Herr |
|
||||||
| `FORM_VORNAME` | First name | Aron |
|
| `FORM_VORNAME` | First name | - |
|
||||||
| `FORM_NACHNAME` | Last name | Petau |
|
| `FORM_NACHNAME` | Last name | - |
|
||||||
| `FORM_EMAIL` | Contact email | `aron@petau.net` |
|
| `FORM_EMAIL` | Contact email | - |
|
||||||
| `FORM_PHONE` | Phone number | 017695773688 |
|
| `FORM_PHONE` | Phone number | - |
|
||||||
|
| `FORM_STRASSE` | Street name | - |
|
||||||
|
| `FORM_HAUSNUMMER` | House number | - |
|
||||||
|
| `FORM_PLZ` | Postal code | - |
|
||||||
|
| `FORM_ORT` | City | Berlin |
|
||||||
| `FORM_PERSONS` | Number of persons moving in | 1 |
|
| `FORM_PERSONS` | Number of persons moving in | 1 |
|
||||||
| `FORM_CHILDREN` | Number of children | 0 |
|
| `FORM_CHILDREN` | Number of children | 0 |
|
||||||
| `FORM_INCOME` | Monthly household net income (€) | 1600 |
|
| `FORM_INCOME` | Monthly household net income (€) | - |
|
||||||
|
|
||||||
|
### Optional - WGcompany.de Search Filters
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `WGCOMPANY_ENABLED` | Enable WGcompany monitoring | true |
|
||||||
|
| `WGCOMPANY_MIN_SIZE` | Minimum room size (m²) | - |
|
||||||
|
| `WGCOMPANY_MAX_SIZE` | Maximum room size (m²) | - |
|
||||||
|
| `WGCOMPANY_MIN_PRICE` | Minimum rent (€) | - |
|
||||||
|
| `WGCOMPANY_MAX_PRICE` | Maximum rent (€) | - |
|
||||||
|
| `WGCOMPANY_BEZIRK` | District code (0=all) | 0 |
|
||||||
|
|
||||||
## Without Login
|
## Without Login
|
||||||
|
|
||||||
|
|
@ -98,10 +125,13 @@ All data is stored in the `./data` directory:
|
||||||
|
|
||||||
| File | Description |
|
| File | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `listings.json` | Known listings (for duplicate detection) |
|
| `listings.json` | Known inberlinwohnen listings |
|
||||||
|
| `wgcompany_listings.json` | Known WGcompany listings |
|
||||||
| `state.json` | Monitor state (autopilot on/off) |
|
| `state.json` | Monitor state (autopilot on/off) |
|
||||||
| `applications.json` | Record of submitted applications |
|
| `applications.json` | Record of submitted applications |
|
||||||
| `listing_times.csv` | Timing data for pattern analysis |
|
| `listing_times.csv` | InBerlin timing data for pattern analysis |
|
||||||
|
| `wgcompany_times.csv` | WGcompany timing data |
|
||||||
| `monitor.log` | Application logs |
|
| `monitor.log` | Application logs |
|
||||||
| `weekly_plot.png` | Generated plot from /plot command |
|
| `weekly_plot.png` | Generated plot from /plot command |
|
||||||
|
| `wgcompany_debug.html` | Debug HTML from WGcompany |
|
||||||
| `*.png` | Screenshots from application attempts |
|
| `*.png` | Screenshots from application attempts |
|
||||||
|
|
|
||||||
BIN
bot_logo.png
Normal file
BIN
bot_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.5 KiB |
3399
debug_page.html
3399
debug_page.html
File diff suppressed because one or more lines are too long
|
|
@ -3,23 +3,7 @@ services:
|
||||||
build: .
|
build: .
|
||||||
container_name: inberlin-monitor
|
container_name: inberlin-monitor
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
env_file:
|
||||||
# Telegram notifications
|
- .env
|
||||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
|
||||||
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}
|
|
||||||
# inberlinwohnen.de login
|
|
||||||
- INBERLIN_EMAIL=${INBERLIN_EMAIL}
|
|
||||||
- INBERLIN_PASSWORD=${INBERLIN_PASSWORD}
|
|
||||||
# Check interval in seconds (default: 300 = 5 minutes)
|
|
||||||
- CHECK_INTERVAL=${CHECK_INTERVAL:-300}
|
|
||||||
# Form data for applications
|
|
||||||
- FORM_ANREDE=${FORM_ANREDE:-Herr}
|
|
||||||
- FORM_VORNAME=${FORM_VORNAME:-Aron}
|
|
||||||
- FORM_NACHNAME=${FORM_NACHNAME:-Petau}
|
|
||||||
- FORM_EMAIL=${FORM_EMAIL:-aron@petau.net}
|
|
||||||
- FORM_PHONE=${FORM_PHONE:-017695773688}
|
|
||||||
- FORM_PERSONS=${FORM_PERSONS:-1}
|
|
||||||
- FORM_CHILDREN=${FORM_CHILDREN:-0}
|
|
||||||
- FORM_INCOME=${FORM_INCOME:-1600}
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
|
|
|
||||||
854
monitor.py
854
monitor.py
|
|
@ -25,15 +25,29 @@ INBERLIN_EMAIL = os.environ.get("INBERLIN_EMAIL", "")
|
||||||
INBERLIN_PASSWORD = os.environ.get("INBERLIN_PASSWORD", "")
|
INBERLIN_PASSWORD = os.environ.get("INBERLIN_PASSWORD", "")
|
||||||
CHECK_INTERVAL = int(os.environ.get("CHECK_INTERVAL", "300")) # seconds (5 minutes)
|
CHECK_INTERVAL = int(os.environ.get("CHECK_INTERVAL", "300")) # seconds (5 minutes)
|
||||||
|
|
||||||
|
# WGcompany search configuration
|
||||||
|
WGCOMPANY_ENABLED = os.environ.get("WGCOMPANY_ENABLED", "true").lower() == "true"
|
||||||
|
WGCOMPANY_MIN_SIZE = os.environ.get("WGCOMPANY_MIN_SIZE", "") # min room size m²
|
||||||
|
WGCOMPANY_MAX_SIZE = os.environ.get("WGCOMPANY_MAX_SIZE", "") # max room size m²
|
||||||
|
WGCOMPANY_MIN_PRICE = os.environ.get("WGCOMPANY_MIN_PRICE", "") # min rent €
|
||||||
|
WGCOMPANY_MAX_PRICE = os.environ.get("WGCOMPANY_MAX_PRICE", "") # max rent €
|
||||||
|
WGCOMPANY_BEZIRK = os.environ.get("WGCOMPANY_BEZIRK", "0") # 0=egal, or specific district code
|
||||||
|
WGCOMPANY_AGE = os.environ.get("WGCOMPANY_AGE", "") # your age (for WG matching)
|
||||||
|
WGCOMPANY_SMOKER = os.environ.get("WGCOMPANY_SMOKER", "") # NR=Nichtraucher, R=Raucher, empty=egal
|
||||||
|
|
||||||
# Form data for applications
|
# Form data for applications
|
||||||
FORM_ANREDE = os.environ.get("FORM_ANREDE", "Herr")
|
FORM_ANREDE = os.environ.get("FORM_ANREDE", "")
|
||||||
FORM_VORNAME = os.environ.get("FORM_VORNAME", "Aron")
|
FORM_VORNAME = os.environ.get("FORM_VORNAME", "")
|
||||||
FORM_NACHNAME = os.environ.get("FORM_NACHNAME", "Petau")
|
FORM_NACHNAME = os.environ.get("FORM_NACHNAME", "")
|
||||||
FORM_EMAIL = os.environ.get("FORM_EMAIL", "aron@petau.net")
|
FORM_EMAIL = os.environ.get("FORM_EMAIL", "")
|
||||||
FORM_PHONE = os.environ.get("FORM_PHONE", "017695773688")
|
FORM_PHONE = os.environ.get("FORM_PHONE", "")
|
||||||
|
FORM_STRASSE = os.environ.get("FORM_STRASSE", "")
|
||||||
|
FORM_HAUSNUMMER = os.environ.get("FORM_HAUSNUMMER", "")
|
||||||
|
FORM_PLZ = os.environ.get("FORM_PLZ", "")
|
||||||
|
FORM_ORT = os.environ.get("FORM_ORT", "")
|
||||||
FORM_PERSONS = os.environ.get("FORM_PERSONS", "1")
|
FORM_PERSONS = os.environ.get("FORM_PERSONS", "1")
|
||||||
FORM_CHILDREN = os.environ.get("FORM_CHILDREN", "0")
|
FORM_CHILDREN = os.environ.get("FORM_CHILDREN", "0")
|
||||||
FORM_INCOME = os.environ.get("FORM_INCOME", "1600")
|
FORM_INCOME = os.environ.get("FORM_INCOME", "")
|
||||||
|
|
||||||
DATA_DIR = Path("/data")
|
DATA_DIR = Path("/data")
|
||||||
LISTINGS_FILE = DATA_DIR / "listings.json"
|
LISTINGS_FILE = DATA_DIR / "listings.json"
|
||||||
|
|
@ -42,6 +56,10 @@ TIMING_FILE = DATA_DIR / "listing_times.csv"
|
||||||
STATE_FILE = DATA_DIR / "state.json"
|
STATE_FILE = DATA_DIR / "state.json"
|
||||||
APPLICATIONS_FILE = DATA_DIR / "applications.json"
|
APPLICATIONS_FILE = DATA_DIR / "applications.json"
|
||||||
|
|
||||||
|
# WGcompany specific files
|
||||||
|
WGCOMPANY_LISTINGS_FILE = DATA_DIR / "wgcompany_listings.json"
|
||||||
|
WGCOMPANY_TIMING_FILE = DATA_DIR / "wgcompany_times.csv"
|
||||||
|
|
||||||
# Setup logging
|
# Setup logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
|
|
@ -108,8 +126,8 @@ class TelegramBot:
|
||||||
self._handle_help_command()
|
self._handle_help_command()
|
||||||
elif text == "/plot":
|
elif text == "/plot":
|
||||||
self._handle_plot_command()
|
self._handle_plot_command()
|
||||||
else:
|
elif text.startswith("/"):
|
||||||
logger.debug(f"Unknown command: {text}")
|
self._handle_unknown_command(text)
|
||||||
|
|
||||||
def _handle_autopilot_command(self, text):
|
def _handle_autopilot_command(self, text):
|
||||||
logger.info(f"Processing autopilot command: {text}")
|
logger.info(f"Processing autopilot command: {text}")
|
||||||
|
|
@ -156,6 +174,10 @@ class TelegramBot:
|
||||||
When autopilot is ON, I will automatically apply to new listings."""
|
When autopilot is ON, I will automatically apply to new listings."""
|
||||||
self._send_message(help_text)
|
self._send_message(help_text)
|
||||||
|
|
||||||
|
def _handle_unknown_command(self, text):
|
||||||
|
cmd = text.split()[0] if text else text
|
||||||
|
self._send_message(f"❓ Unknown command: <code>{cmd}</code>\n\nUse /help to see available commands.")
|
||||||
|
|
||||||
def _handle_plot_command(self):
|
def _handle_plot_command(self):
|
||||||
"""Generate and send a plot of listing times"""
|
"""Generate and send a plot of listing times"""
|
||||||
logger.info("Generating listing times plot...")
|
logger.info("Generating listing times plot...")
|
||||||
|
|
@ -375,12 +397,56 @@ class ApplicationHandler:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
# Look for "Besichtigung vereinbaren" button
|
# Try to handle consent manager (consentmanager.net)
|
||||||
logger.info("[HOWOGE] Looking for 'Besichtigung vereinbaren' button...")
|
try:
|
||||||
apply_btn = await page.query_selector('a:has-text("Besichtigung vereinbaren"), button:has-text("Besichtigung vereinbaren"), a:has-text("Anfragen"), button:has-text("Anfragen")')
|
consent_selectors = [
|
||||||
|
'#cmpbntyestxt', '.cmpboxbtnyes', 'a.cmpboxbtn.cmpboxbtnyes',
|
||||||
|
'#cmpwelcomebtnyes', '.cmptxt_btn_yes'
|
||||||
|
]
|
||||||
|
for sel in consent_selectors:
|
||||||
|
consent_btn = await page.query_selector(sel)
|
||||||
|
if consent_btn and await consent_btn.is_visible():
|
||||||
|
await consent_btn.click()
|
||||||
|
logger.info("[HOWOGE] Dismissed consent manager")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
break
|
||||||
|
except: pass
|
||||||
|
|
||||||
if apply_btn and await apply_btn.is_visible():
|
# Look for "Besichtigung vereinbaren" button
|
||||||
logger.info("[HOWOGE] Found application button, clicking...")
|
# HOWOGE has multiple buttons with same text - only one is visible
|
||||||
|
logger.info("[HOWOGE] Looking for 'Besichtigung vereinbaren' button...")
|
||||||
|
|
||||||
|
# Use href selector - more reliable than text matching
|
||||||
|
selectors = [
|
||||||
|
'a[href*="besichtigung-vereinbaren"]',
|
||||||
|
'a:has-text("Besichtigung vereinbaren")',
|
||||||
|
'button:has-text("Besichtigung vereinbaren")',
|
||||||
|
'a:has-text("Anfragen")',
|
||||||
|
'button:has-text("Anfragen")'
|
||||||
|
]
|
||||||
|
|
||||||
|
apply_btn = None
|
||||||
|
for sel in selectors:
|
||||||
|
all_btns = await page.query_selector_all(sel)
|
||||||
|
logger.info(f"[HOWOGE] Selector '{sel}' found {len(all_btns)} matches")
|
||||||
|
# Find first visible button
|
||||||
|
for btn in all_btns:
|
||||||
|
try:
|
||||||
|
if await btn.is_visible():
|
||||||
|
apply_btn = btn
|
||||||
|
logger.info(f"[HOWOGE] Found visible button with selector '{sel}'")
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if apply_btn:
|
||||||
|
break
|
||||||
|
|
||||||
|
if apply_btn:
|
||||||
|
# Scroll the button into view and click
|
||||||
|
logger.info("[HOWOGE] Found application button, scrolling into view...")
|
||||||
|
await apply_btn.scroll_into_view_if_needed()
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
logger.info("[HOWOGE] Clicking button...")
|
||||||
await apply_btn.click()
|
await apply_btn.click()
|
||||||
await asyncio.sleep(3)
|
await asyncio.sleep(3)
|
||||||
await page.wait_for_load_state("networkidle")
|
await page.wait_for_load_state("networkidle")
|
||||||
|
|
@ -520,6 +586,11 @@ class ApplicationHandler:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def _apply_degewo(self, listing: dict, result: dict) -> dict:
|
async def _apply_degewo(self, listing: dict, result: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Degewo uses Wohnungshelden (app.wohnungshelden.de) for their application system.
|
||||||
|
The application form is loaded in an iframe from a different domain.
|
||||||
|
We need to navigate directly to the iframe URL or interact with the iframe.
|
||||||
|
"""
|
||||||
page = await self.context.new_page()
|
page = await self.context.new_page()
|
||||||
try:
|
try:
|
||||||
logger.info(f"[DEGEWO] Opening page: {listing['link']}")
|
logger.info(f"[DEGEWO] Opening page: {listing['link']}")
|
||||||
|
|
@ -527,6 +598,7 @@ class ApplicationHandler:
|
||||||
logger.info("[DEGEWO] Page loaded")
|
logger.info("[DEGEWO] Page loaded")
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
# Dismiss cookie banner
|
||||||
try:
|
try:
|
||||||
cookie_btn = await page.query_selector('button:has-text("Alle akzeptieren"), #CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll')
|
cookie_btn = await page.query_selector('button:has-text("Alle akzeptieren"), #CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll')
|
||||||
if cookie_btn and await cookie_btn.is_visible():
|
if cookie_btn and await cookie_btn.is_visible():
|
||||||
|
|
@ -542,155 +614,186 @@ class ApplicationHandler:
|
||||||
await apply_btn.click()
|
await apply_btn.click()
|
||||||
await asyncio.sleep(3)
|
await asyncio.sleep(3)
|
||||||
|
|
||||||
# Fill out the contact form
|
# Degewo uses Wohnungshelden iframe for the application form
|
||||||
logger.info("[DEGEWO] Filling out contact form...")
|
# Find the iframe and get its URL to navigate directly
|
||||||
|
iframe_element = await page.query_selector('iframe[src*="wohnungshelden.de"]')
|
||||||
|
if iframe_element:
|
||||||
|
iframe_url = await iframe_element.get_attribute('src')
|
||||||
|
logger.info(f"[DEGEWO] Found Wohnungshelden iframe: {iframe_url}")
|
||||||
|
|
||||||
# Anrede - select from env
|
# Navigate to the iframe URL directly in a new page for full access
|
||||||
try:
|
iframe_page = await self.context.new_page()
|
||||||
anrede_select = await page.query_selector('select[name*="anrede"], select[name*="salutation"], select[id*="anrede"]')
|
try:
|
||||||
if anrede_select:
|
await iframe_page.goto(iframe_url, wait_until="networkidle")
|
||||||
await anrede_select.select_option(label=FORM_ANREDE)
|
await asyncio.sleep(2)
|
||||||
logger.info(f"[DEGEWO] Selected Anrede: {FORM_ANREDE}")
|
logger.info("[DEGEWO] Loaded Wohnungshelden application page")
|
||||||
else:
|
|
||||||
# Try radio button
|
|
||||||
anrede_radio = await page.query_selector(f'input[type="radio"][value="{FORM_ANREDE}"], label:has-text("{FORM_ANREDE}") input[type="radio"]')
|
|
||||||
if anrede_radio:
|
|
||||||
await anrede_radio.click()
|
|
||||||
logger.info(f"[DEGEWO] Clicked Anrede radio: {FORM_ANREDE}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"[DEGEWO] Could not set Anrede: {e}")
|
|
||||||
|
|
||||||
# Vorname
|
# Take screenshot of the Wohnungshelden form
|
||||||
try:
|
screenshot_path = DATA_DIR / f"degewo_wohnungshelden_{listing['id']}.png"
|
||||||
vorname_field = await page.query_selector('input[name*="vorname"], input[name*="firstname"], input[id*="vorname"], input[placeholder*="Vorname"]')
|
await iframe_page.screenshot(path=str(screenshot_path), full_page=True)
|
||||||
if vorname_field:
|
logger.info(f"[DEGEWO] Saved Wohnungshelden screenshot to {screenshot_path}")
|
||||||
await vorname_field.fill(FORM_VORNAME)
|
|
||||||
logger.info(f"[DEGEWO] Filled Vorname: {FORM_VORNAME}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"[DEGEWO] Could not fill Vorname: {e}")
|
|
||||||
|
|
||||||
# Nachname
|
# Save HTML for debugging
|
||||||
try:
|
html_content = await iframe_page.content()
|
||||||
nachname_field = await page.query_selector('input[name*="nachname"], input[name*="lastname"], input[id*="nachname"], input[placeholder*="Nachname"]')
|
html_path = DATA_DIR / f"degewo_wohnungshelden_{listing['id']}.html"
|
||||||
if nachname_field:
|
with open(html_path, 'w', encoding='utf-8') as f:
|
||||||
await nachname_field.fill(FORM_NACHNAME)
|
f.write(html_content)
|
||||||
logger.info(f"[DEGEWO] Filled Nachname: {FORM_NACHNAME}")
|
logger.info(f"[DEGEWO] Saved HTML to {html_path}")
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"[DEGEWO] Could not fill Nachname: {e}")
|
|
||||||
|
|
||||||
# E-Mail
|
# Fill out Wohnungshelden form
|
||||||
try:
|
# The form uses specific IDs: #firstName, #lastName, #email, etc.
|
||||||
email_field = await page.query_selector('input[type="email"], input[name*="email"], input[name*="mail"], input[id*="email"]')
|
form_filled = False
|
||||||
if email_field:
|
|
||||||
await email_field.fill(FORM_EMAIL)
|
|
||||||
logger.info(f"[DEGEWO] Filled E-Mail: {FORM_EMAIL}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"[DEGEWO] Could not fill E-Mail: {e}")
|
|
||||||
|
|
||||||
# Telefonnummer
|
# Anrede (Salutation) - ng-select dropdown
|
||||||
try:
|
try:
|
||||||
tel_field = await page.query_selector('input[type="tel"], input[name*="telefon"], input[name*="phone"], input[id*="telefon"]')
|
# Click on the salutation dropdown to open it
|
||||||
if tel_field:
|
salutation_dropdown = await iframe_page.query_selector('#salutation-dropdown, ng-select[id*="salutation"]')
|
||||||
await tel_field.fill(FORM_PHONE)
|
if salutation_dropdown:
|
||||||
logger.info(f"[DEGEWO] Filled Telefonnummer: {FORM_PHONE}")
|
await salutation_dropdown.click()
|
||||||
except Exception as e:
|
await asyncio.sleep(0.5)
|
||||||
logger.warning(f"[DEGEWO] Could not handle Telefon: {e}")
|
# Select "Herr" or "Frau" based on FORM_ANREDE
|
||||||
|
anrede_option = await iframe_page.query_selector(f'.ng-option:has-text("{FORM_ANREDE}")')
|
||||||
|
if anrede_option:
|
||||||
|
await anrede_option.click()
|
||||||
|
logger.info(f"[DEGEWO] Selected Anrede: {FORM_ANREDE}")
|
||||||
|
form_filled = True
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[DEGEWO] Could not set Anrede: {e}")
|
||||||
|
|
||||||
# Anzahl einziehende Personen
|
# Vorname (First name)
|
||||||
try:
|
try:
|
||||||
personen_field = await page.query_selector('input[name*="personen"], input[name*="persons"], input[id*="personen"], select[name*="personen"]')
|
vorname_field = await iframe_page.query_selector('#firstName')
|
||||||
if personen_field:
|
if vorname_field:
|
||||||
tag_name = await personen_field.evaluate("el => el.tagName.toLowerCase()")
|
await vorname_field.fill(FORM_VORNAME)
|
||||||
if tag_name == "select":
|
logger.info(f"[DEGEWO] Filled Vorname: {FORM_VORNAME}")
|
||||||
await personen_field.select_option(FORM_PERSONS)
|
form_filled = True
|
||||||
else:
|
except Exception as e:
|
||||||
await personen_field.fill(FORM_PERSONS)
|
logger.warning(f"[DEGEWO] Could not fill Vorname: {e}")
|
||||||
logger.info(f"[DEGEWO] Set Anzahl Personen: {FORM_PERSONS}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"[DEGEWO] Could not set Personen: {e}")
|
|
||||||
|
|
||||||
# davon Anzahl Kinder
|
# Nachname (Last name)
|
||||||
try:
|
try:
|
||||||
kinder_field = await page.query_selector('input[name*="kinder"], input[name*="children"], input[id*="kinder"], select[name*="kinder"]')
|
nachname_field = await iframe_page.query_selector('#lastName')
|
||||||
if kinder_field:
|
if nachname_field:
|
||||||
tag_name = await kinder_field.evaluate("el => el.tagName.toLowerCase()")
|
await nachname_field.fill(FORM_NACHNAME)
|
||||||
if tag_name == "select":
|
logger.info(f"[DEGEWO] Filled Nachname: {FORM_NACHNAME}")
|
||||||
await kinder_field.select_option(FORM_CHILDREN)
|
form_filled = True
|
||||||
else:
|
except Exception as e:
|
||||||
await kinder_field.fill(FORM_CHILDREN)
|
logger.warning(f"[DEGEWO] Could not fill Nachname: {e}")
|
||||||
logger.info(f"[DEGEWO] Set Anzahl Kinder: {FORM_CHILDREN}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"[DEGEWO] Could not set Kinder: {e}")
|
|
||||||
|
|
||||||
# Monatliches Haushaltsnettoeinkommen
|
# E-Mail
|
||||||
try:
|
try:
|
||||||
einkommen_field = await page.query_selector('input[name*="einkommen"], input[name*="income"], input[id*="einkommen"], select[name*="einkommen"]')
|
email_field = await iframe_page.query_selector('#email')
|
||||||
if einkommen_field:
|
if email_field:
|
||||||
tag_name = await einkommen_field.evaluate("el => el.tagName.toLowerCase()")
|
await email_field.fill(FORM_EMAIL)
|
||||||
if tag_name == "select":
|
logger.info(f"[DEGEWO] Filled E-Mail: {FORM_EMAIL}")
|
||||||
# Try to select by value or index
|
form_filled = True
|
||||||
try:
|
except Exception as e:
|
||||||
await einkommen_field.select_option(FORM_INCOME)
|
logger.warning(f"[DEGEWO] Could not fill E-Mail: {e}")
|
||||||
except:
|
|
||||||
# Fallback to first non-empty option
|
|
||||||
options = await einkommen_field.query_selector_all("option")
|
|
||||||
if len(options) > 1:
|
|
||||||
await einkommen_field.select_option(index=1)
|
|
||||||
else:
|
|
||||||
await einkommen_field.fill(FORM_INCOME)
|
|
||||||
logger.info(f"[DEGEWO] Set Einkommen: {FORM_INCOME}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"[DEGEWO] Could not set Einkommen: {e}")
|
|
||||||
|
|
||||||
# "Für mich selbst" selection
|
# Telefonnummer
|
||||||
try:
|
try:
|
||||||
selbst_radio = await page.query_selector('input[type="radio"][value*="selbst"], input[type="radio"][value*="myself"], label:has-text("Für mich selbst") input')
|
tel_field = await iframe_page.query_selector('input[id*="telefonnummer"]')
|
||||||
if selbst_radio:
|
if tel_field:
|
||||||
await selbst_radio.click()
|
await tel_field.fill(FORM_PHONE)
|
||||||
logger.info("[DEGEWO] Selected: Für mich selbst")
|
logger.info(f"[DEGEWO] Filled Telefon: {FORM_PHONE}")
|
||||||
except Exception as e:
|
form_filled = True
|
||||||
logger.warning(f"[DEGEWO] Could not set 'Für mich selbst': {e}")
|
except Exception as e:
|
||||||
|
logger.warning(f"[DEGEWO] Could not fill Telefon: {e}")
|
||||||
|
|
||||||
# Accept data privacy checkbox
|
# Anzahl einziehende Personen
|
||||||
try:
|
try:
|
||||||
checkbox = await page.query_selector('input[type="checkbox"][name*="datenschutz"], input[type="checkbox"][name*="privacy"], input[type="checkbox"][name*="consent"]')
|
personen_field = await iframe_page.query_selector('input[id*="numberPersonsTotal"]')
|
||||||
if checkbox and not await checkbox.is_checked():
|
if personen_field:
|
||||||
await checkbox.click()
|
await personen_field.fill(FORM_PERSONS)
|
||||||
logger.info("[DEGEWO] Checked privacy/consent checkbox")
|
logger.info(f"[DEGEWO] Filled Anzahl Personen: {FORM_PERSONS}")
|
||||||
except Exception as e:
|
form_filled = True
|
||||||
logger.warning(f"[DEGEWO] Could not check consent: {e}")
|
except Exception as e:
|
||||||
|
logger.warning(f"[DEGEWO] Could not fill Anzahl Personen: {e}")
|
||||||
|
|
||||||
await asyncio.sleep(1)
|
# "Für sich selbst" dropdown
|
||||||
|
try:
|
||||||
|
selbst_dropdown = await iframe_page.query_selector('ng-select[id*="fuer_wen"]')
|
||||||
|
if selbst_dropdown:
|
||||||
|
await selbst_dropdown.click()
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
# Select "Für mich selbst"
|
||||||
|
selbst_option = await iframe_page.query_selector('.ng-option:has-text("Für mich selbst"), .ng-option:has-text("selbst")')
|
||||||
|
if selbst_option:
|
||||||
|
await selbst_option.click()
|
||||||
|
logger.info("[DEGEWO] Selected: Für mich selbst")
|
||||||
|
form_filled = True
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[DEGEWO] Could not set 'Für sich selbst': {e}")
|
||||||
|
|
||||||
# Take screenshot before submitting
|
await asyncio.sleep(1)
|
||||||
screenshot_path = DATA_DIR / f"degewo_form_{listing['id']}.png"
|
|
||||||
await page.screenshot(path=str(screenshot_path), full_page=True)
|
|
||||||
logger.info(f"[DEGEWO] Saved form screenshot to {screenshot_path}")
|
|
||||||
|
|
||||||
# Submit the form
|
# Take screenshot after filling form
|
||||||
try:
|
screenshot_path = DATA_DIR / f"degewo_form_filled_{listing['id']}.png"
|
||||||
submit_btn = await page.query_selector('button[type="submit"], input[type="submit"], button:has-text("Absenden"), button:has-text("Senden")')
|
await iframe_page.screenshot(path=str(screenshot_path), full_page=True)
|
||||||
if submit_btn and await submit_btn.is_visible():
|
logger.info(f"[DEGEWO] Saved filled form screenshot to {screenshot_path}")
|
||||||
await submit_btn.click()
|
|
||||||
logger.info("[DEGEWO] Clicked submit button")
|
|
||||||
await asyncio.sleep(3)
|
|
||||||
|
|
||||||
# Take screenshot after submission
|
# Try to submit
|
||||||
screenshot_path = DATA_DIR / f"degewo_submitted_{listing['id']}.png"
|
try:
|
||||||
await page.screenshot(path=str(screenshot_path), full_page=True)
|
# Look for submit button with various patterns
|
||||||
logger.info(f"[DEGEWO] Saved submission screenshot to {screenshot_path}")
|
submit_selectors = [
|
||||||
|
'button[type="submit"]',
|
||||||
|
'input[type="submit"]',
|
||||||
|
'button:has-text("Absenden")',
|
||||||
|
'button:has-text("Senden")',
|
||||||
|
'button:has-text("Anfrage")',
|
||||||
|
'button:has-text("Bewerben")',
|
||||||
|
'button:has-text("Submit")',
|
||||||
|
'.btn-primary',
|
||||||
|
'.submit-btn',
|
||||||
|
]
|
||||||
|
|
||||||
result["success"] = True
|
submit_btn = None
|
||||||
result["message"] = "Application submitted"
|
for selector in submit_selectors:
|
||||||
else:
|
submit_btn = await iframe_page.query_selector(selector)
|
||||||
result["success"] = True
|
if submit_btn and await submit_btn.is_visible():
|
||||||
result["message"] = "Form filled, submit button not found"
|
logger.info(f"[DEGEWO] Found submit button with selector: {selector}")
|
||||||
logger.warning("[DEGEWO] Submit button not found")
|
break
|
||||||
except Exception as e:
|
submit_btn = None
|
||||||
result["success"] = True
|
|
||||||
result["message"] = f"Form filled, submit error: {str(e)}"
|
|
||||||
logger.warning(f"[DEGEWO] Submit error: {e}")
|
|
||||||
|
|
||||||
|
if submit_btn:
|
||||||
|
await submit_btn.click()
|
||||||
|
logger.info("[DEGEWO] Clicked submit button")
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
|
||||||
|
# Take screenshot after submission
|
||||||
|
screenshot_path = DATA_DIR / f"degewo_submitted_{listing['id']}.png"
|
||||||
|
await iframe_page.screenshot(path=str(screenshot_path), full_page=True)
|
||||||
|
logger.info(f"[DEGEWO] Saved submission screenshot to {screenshot_path}")
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["message"] = "Application submitted via Wohnungshelden"
|
||||||
|
else:
|
||||||
|
# Submit button not found - this is a failure
|
||||||
|
result["success"] = False
|
||||||
|
result["message"] = "Wohnungshelden form loaded but submit button not found"
|
||||||
|
logger.warning("[DEGEWO] Submit button not found in Wohnungshelden form")
|
||||||
|
except Exception as e:
|
||||||
|
result["success"] = False
|
||||||
|
result["message"] = f"Wohnungshelden submit error: {str(e)}"
|
||||||
|
logger.warning(f"[DEGEWO] Submit error: {e}")
|
||||||
|
finally:
|
||||||
|
await iframe_page.close()
|
||||||
|
else:
|
||||||
|
# No iframe found - try the old approach (fallback for different page structure)
|
||||||
|
logger.warning("[DEGEWO] Wohnungshelden iframe not found, trying direct form...")
|
||||||
|
|
||||||
|
# Take screenshot for debugging
|
||||||
|
screenshot_path = DATA_DIR / f"degewo_noiframe_{listing['id']}.png"
|
||||||
|
await page.screenshot(path=str(screenshot_path), full_page=True)
|
||||||
|
|
||||||
|
# Save HTML for debugging
|
||||||
|
html_content = await page.content()
|
||||||
|
html_path = DATA_DIR / "degewo_debug.html"
|
||||||
|
with open(html_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(html_content)
|
||||||
|
|
||||||
|
result["success"] = False
|
||||||
|
result["message"] = "Wohnungshelden iframe not found on page"
|
||||||
else:
|
else:
|
||||||
result["message"] = "No kontaktieren button found"
|
result["message"] = "No kontaktieren button found"
|
||||||
logger.warning("[DEGEWO] Could not find kontaktieren button")
|
logger.warning("[DEGEWO] Could not find kontaktieren button")
|
||||||
|
|
@ -762,22 +865,138 @@ class ApplicationHandler:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
logger.info("[STADTUNDLAND] Looking for application button...")
|
# Stadt und Land has the contact form directly on the page
|
||||||
apply_btn = await page.query_selector('a:has-text("Anfragen"), button:has-text("Bewerben"), a:has-text("Interesse")')
|
logger.info("[STADTUNDLAND] Looking for contact form fields...")
|
||||||
if apply_btn and await apply_btn.is_visible():
|
|
||||||
logger.info("[STADTUNDLAND] Found application button, clicking...")
|
|
||||||
await apply_btn.click()
|
|
||||||
await asyncio.sleep(2)
|
|
||||||
|
|
||||||
screenshot_path = DATA_DIR / f"stadtundland_{listing['id']}.png"
|
form_filled = False
|
||||||
await page.screenshot(path=str(screenshot_path))
|
|
||||||
logger.info(f"[STADTUNDLAND] Saved screenshot to {screenshot_path}")
|
|
||||||
|
|
||||||
result["success"] = True
|
# Fill Vorname
|
||||||
result["message"] = "Application page opened"
|
try:
|
||||||
|
vorname_field = await page.query_selector('input[name*="vorname" i], input[placeholder*="Vorname" i], input#vorname')
|
||||||
|
if vorname_field:
|
||||||
|
await vorname_field.fill(FORM_VORNAME)
|
||||||
|
logger.info(f"[STADTUNDLAND] Filled Vorname: {FORM_VORNAME}")
|
||||||
|
form_filled = True
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[STADTUNDLAND] Could not fill Vorname: {e}")
|
||||||
|
|
||||||
|
# Fill Nachname
|
||||||
|
try:
|
||||||
|
nachname_field = await page.query_selector('input[name*="nachname" i], input[placeholder*="Nachname" i], input#nachname')
|
||||||
|
if nachname_field:
|
||||||
|
await nachname_field.fill(FORM_NACHNAME)
|
||||||
|
logger.info(f"[STADTUNDLAND] Filled Nachname: {FORM_NACHNAME}")
|
||||||
|
form_filled = True
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[STADTUNDLAND] Could not fill Nachname: {e}")
|
||||||
|
|
||||||
|
# Fill Telefonnummer
|
||||||
|
try:
|
||||||
|
tel_field = await page.query_selector('input[name*="telefon" i], input[type="tel"], input[placeholder*="Telefon" i]')
|
||||||
|
if tel_field:
|
||||||
|
await tel_field.fill(FORM_PHONE)
|
||||||
|
logger.info(f"[STADTUNDLAND] Filled Telefon: {FORM_PHONE}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[STADTUNDLAND] Could not fill Telefon: {e}")
|
||||||
|
|
||||||
|
# Fill E-Mail
|
||||||
|
try:
|
||||||
|
email_field = await page.query_selector('input[type="email"], input[name*="email" i], input[name*="mail" i]')
|
||||||
|
if email_field:
|
||||||
|
await email_field.fill(FORM_EMAIL)
|
||||||
|
logger.info(f"[STADTUNDLAND] Filled E-Mail: {FORM_EMAIL}")
|
||||||
|
form_filled = True
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[STADTUNDLAND] Could not fill E-Mail: {e}")
|
||||||
|
|
||||||
|
# Fill Straße (street)
|
||||||
|
try:
|
||||||
|
strasse_field = await page.query_selector('input[name*="strasse" i], input[name*="straße" i], input[placeholder*="Straße" i], input#strasse')
|
||||||
|
if strasse_field and FORM_STRASSE:
|
||||||
|
await strasse_field.fill(FORM_STRASSE)
|
||||||
|
logger.info(f"[STADTUNDLAND] Filled Straße: {FORM_STRASSE}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[STADTUNDLAND] Could not fill Straße: {e}")
|
||||||
|
|
||||||
|
# Fill Hausnummer
|
||||||
|
try:
|
||||||
|
hausnummer_field = await page.query_selector('input[name*="hausnummer" i], input[name*="hausnr" i], input[placeholder*="Hausnummer" i], input#hausnummer')
|
||||||
|
if hausnummer_field and FORM_HAUSNUMMER:
|
||||||
|
await hausnummer_field.fill(FORM_HAUSNUMMER)
|
||||||
|
logger.info(f"[STADTUNDLAND] Filled Hausnummer: {FORM_HAUSNUMMER}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[STADTUNDLAND] Could not fill Hausnummer: {e}")
|
||||||
|
|
||||||
|
# Fill PLZ
|
||||||
|
try:
|
||||||
|
plz_field = await page.query_selector('input[name*="plz" i], input[placeholder*="PLZ" i], input#plz')
|
||||||
|
if plz_field and FORM_PLZ:
|
||||||
|
await plz_field.fill(FORM_PLZ)
|
||||||
|
logger.info(f"[STADTUNDLAND] Filled PLZ: {FORM_PLZ}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[STADTUNDLAND] Could not fill PLZ: {e}")
|
||||||
|
|
||||||
|
# Fill Ort (city)
|
||||||
|
try:
|
||||||
|
ort_field = await page.query_selector('input[name*="ort" i], input[placeholder*="Ort" i], input#ort')
|
||||||
|
if ort_field and FORM_ORT:
|
||||||
|
await ort_field.fill(FORM_ORT)
|
||||||
|
logger.info(f"[STADTUNDLAND] Filled Ort: {FORM_ORT}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[STADTUNDLAND] Could not fill Ort: {e}")
|
||||||
|
|
||||||
|
# Check Datenschutz checkbox
|
||||||
|
try:
|
||||||
|
datenschutz_checkbox = await page.query_selector('input[type="checkbox"][name*="datenschutz" i], input[type="checkbox"][name*="privacy" i]')
|
||||||
|
if datenschutz_checkbox and not await datenschutz_checkbox.is_checked():
|
||||||
|
await datenschutz_checkbox.click()
|
||||||
|
logger.info("[STADTUNDLAND] Checked Datenschutz checkbox")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[STADTUNDLAND] Could not check Datenschutz: {e}")
|
||||||
|
|
||||||
|
# Check Provision checkbox
|
||||||
|
try:
|
||||||
|
provision_checkbox = await page.query_selector('input[type="checkbox"][name*="provision" i]')
|
||||||
|
if provision_checkbox and not await provision_checkbox.is_checked():
|
||||||
|
await provision_checkbox.click()
|
||||||
|
logger.info("[STADTUNDLAND] Checked Provision checkbox")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[STADTUNDLAND] Could not check Provision: {e}")
|
||||||
|
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
# Screenshot before submitting
|
||||||
|
screenshot_path = DATA_DIR / f"stadtundland_form_{listing['id']}.png"
|
||||||
|
await page.screenshot(path=str(screenshot_path), full_page=True)
|
||||||
|
logger.info(f"[STADTUNDLAND] Saved form screenshot to {screenshot_path}")
|
||||||
|
|
||||||
|
if form_filled:
|
||||||
|
# Submit the form - look for submit button
|
||||||
|
try:
|
||||||
|
submit_btn = await page.query_selector('button[type="submit"], input[type="submit"], button:has-text("prüfen"), button:has-text("Absenden"), button:has-text("Senden")')
|
||||||
|
if submit_btn and await submit_btn.is_visible():
|
||||||
|
await submit_btn.click()
|
||||||
|
logger.info("[STADTUNDLAND] Clicked submit button")
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
|
||||||
|
# Screenshot after submission
|
||||||
|
screenshot_path = DATA_DIR / f"stadtundland_submitted_{listing['id']}.png"
|
||||||
|
await page.screenshot(path=str(screenshot_path), full_page=True)
|
||||||
|
logger.info(f"[STADTUNDLAND] Saved submission screenshot to {screenshot_path}")
|
||||||
|
|
||||||
|
result["success"] = True
|
||||||
|
result["message"] = "Application submitted"
|
||||||
|
else:
|
||||||
|
result["success"] = True
|
||||||
|
result["message"] = "Form filled, submit button not found"
|
||||||
|
logger.warning("[STADTUNDLAND] Submit button not found")
|
||||||
|
except Exception as e:
|
||||||
|
result["success"] = True
|
||||||
|
result["message"] = f"Form filled, submit error: {str(e)}"
|
||||||
|
logger.warning(f"[STADTUNDLAND] Submit error: {e}")
|
||||||
else:
|
else:
|
||||||
result["message"] = "No application button found"
|
result["message"] = "No form fields found"
|
||||||
logger.warning("[STADTUNDLAND] Could not find application button")
|
logger.warning("[STADTUNDLAND] Could not find form fields")
|
||||||
screenshot_path = DATA_DIR / f"stadtundland_nobtn_{listing['id']}.png"
|
screenshot_path = DATA_DIR / f"stadtundland_nobtn_{listing['id']}.png"
|
||||||
await page.screenshot(path=str(screenshot_path))
|
await page.screenshot(path=str(screenshot_path))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -1284,26 +1503,331 @@ class InBerlinMonitor:
|
||||||
return await self.apply_to_listings(listings)
|
return await self.apply_to_listings(listings)
|
||||||
|
|
||||||
|
|
||||||
|
class WGCompanyMonitor:
|
||||||
|
"""Monitor WGcompany.de for new WG room listings"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.browser = None
|
||||||
|
self.context = None
|
||||||
|
|
||||||
|
async def init_browser(self):
|
||||||
|
"""Initialize Playwright browser"""
|
||||||
|
if self.browser is None:
|
||||||
|
self.playwright = await async_playwright().start()
|
||||||
|
self.browser = await self.playwright.chromium.launch(headless=True)
|
||||||
|
self.context = await self.browser.new_context(
|
||||||
|
user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
|
||||||
|
)
|
||||||
|
logger.info("[WGCOMPANY] Browser initialized")
|
||||||
|
|
||||||
|
async def fetch_listings(self) -> list[dict]:
|
||||||
|
"""Fetch WG listings from wgcompany.de search"""
|
||||||
|
listings = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
page = await self.context.new_page()
|
||||||
|
|
||||||
|
# Use simple search page: st=1 (Berlin), mi=10 (simple WG search), li=100
|
||||||
|
search_url = "http://www.wgcompany.de/cgi-bin/seite?st=1&mi=10&li=100"
|
||||||
|
logger.info(f"[WGCOMPANY] Loading search page: {search_url}")
|
||||||
|
await page.goto(search_url, wait_until="networkidle")
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
# Fill search form - field names from simple search:
|
||||||
|
# c = Min. Größe (min size m²)
|
||||||
|
# a = Max. Miete (max rent €)
|
||||||
|
# l = Alter (age)
|
||||||
|
# e = Bezirk (district select)
|
||||||
|
|
||||||
|
# Min size field
|
||||||
|
if WGCOMPANY_MIN_SIZE:
|
||||||
|
min_size_field = await page.query_selector('input[name="c"]')
|
||||||
|
if min_size_field:
|
||||||
|
await min_size_field.fill(WGCOMPANY_MIN_SIZE)
|
||||||
|
logger.info(f"[WGCOMPANY] Set min size: {WGCOMPANY_MIN_SIZE} m²")
|
||||||
|
|
||||||
|
# Max rent field
|
||||||
|
if WGCOMPANY_MAX_PRICE:
|
||||||
|
max_price_field = await page.query_selector('input[name="a"]')
|
||||||
|
if max_price_field:
|
||||||
|
await max_price_field.fill(WGCOMPANY_MAX_PRICE)
|
||||||
|
logger.info(f"[WGCOMPANY] Set max rent: {WGCOMPANY_MAX_PRICE} €")
|
||||||
|
|
||||||
|
# Age field (l = Alter)
|
||||||
|
if WGCOMPANY_AGE:
|
||||||
|
age_field = await page.query_selector('input[name="l"]')
|
||||||
|
if age_field:
|
||||||
|
await age_field.fill(WGCOMPANY_AGE)
|
||||||
|
logger.info(f"[WGCOMPANY] Set age: {WGCOMPANY_AGE}")
|
||||||
|
|
||||||
|
# Smoker filter (o = RaucherIn: NR=Nichtraucher, R=Raucher)
|
||||||
|
if WGCOMPANY_SMOKER:
|
||||||
|
smoker_select = await page.query_selector('select[name="o"]')
|
||||||
|
if smoker_select:
|
||||||
|
await smoker_select.select_option(WGCOMPANY_SMOKER)
|
||||||
|
logger.info(f"[WGCOMPANY] Set smoker: {WGCOMPANY_SMOKER}")
|
||||||
|
|
||||||
|
# District selection (e = Bezirk, multi-select)
|
||||||
|
# Leave as default "egal" (all districts) unless specified
|
||||||
|
if WGCOMPANY_BEZIRK and WGCOMPANY_BEZIRK != "0":
|
||||||
|
bezirk_select = await page.query_selector('select[name="e"]')
|
||||||
|
if bezirk_select:
|
||||||
|
await bezirk_select.select_option(WGCOMPANY_BEZIRK)
|
||||||
|
logger.info(f"[WGCOMPANY] Set district: {WGCOMPANY_BEZIRK}")
|
||||||
|
|
||||||
|
# Submit the search form
|
||||||
|
submit_btn = await page.query_selector('input[type="submit"][value*="finde"], input[type="submit"]')
|
||||||
|
if submit_btn:
|
||||||
|
logger.info("[WGCOMPANY] Submitting search form...")
|
||||||
|
await submit_btn.click()
|
||||||
|
await page.wait_for_load_state("networkidle")
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
# Get results page content
|
||||||
|
content = await page.content()
|
||||||
|
|
||||||
|
# Save debug HTML
|
||||||
|
debug_path = DATA_DIR / "wgcompany_debug.html"
|
||||||
|
with open(debug_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
logger.info(f"[WGCOMPANY] Saved debug HTML to {debug_path}")
|
||||||
|
|
||||||
|
# Parse listings from the results page
|
||||||
|
# WGcompany results typically have tables with room info
|
||||||
|
# Look for listing links and extract data
|
||||||
|
|
||||||
|
# Pattern to find listing detail links
|
||||||
|
# Format: wg.pl?...function=wgzeigen... with room details in table rows
|
||||||
|
listing_links = await page.query_selector_all('a[href*="wg.pl"][href*="wgzeigen"]')
|
||||||
|
logger.info(f"[WGCOMPANY] Found {len(listing_links)} listing links")
|
||||||
|
|
||||||
|
for link_elem in listing_links:
|
||||||
|
try:
|
||||||
|
href = await link_elem.get_attribute("href")
|
||||||
|
if not href:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get surrounding text/row for listing details
|
||||||
|
parent = await link_elem.evaluate_handle("el => el.closest('tr') || el.parentElement")
|
||||||
|
row_text = await parent.evaluate("el => el.innerText") if parent else ""
|
||||||
|
|
||||||
|
# Extract price from row text (e.g., "350 €" or "350€")
|
||||||
|
price_match = re.search(r'(\d+)\s*€', row_text)
|
||||||
|
price = price_match.group(1) + " €" if price_match else "?"
|
||||||
|
|
||||||
|
# Extract size (e.g., "15 m²" or "15m²")
|
||||||
|
size_match = re.search(r'(\d+)\s*m²', row_text)
|
||||||
|
size = size_match.group(1) + " m²" if size_match else "?"
|
||||||
|
|
||||||
|
# Extract district/location
|
||||||
|
# Common Berlin districts in text
|
||||||
|
bezirk_patterns = [
|
||||||
|
"Kreuzberg", "Neukölln", "Friedrichshain", "Prenzlauer Berg",
|
||||||
|
"Mitte", "Wedding", "Charlottenburg", "Schöneberg", "Tempelhof",
|
||||||
|
"Steglitz", "Wilmersdorf", "Pankow", "Lichtenberg", "Treptow",
|
||||||
|
"Köpenick", "Reinickendorf", "Spandau", "Zehlendorf", "Moabit"
|
||||||
|
]
|
||||||
|
location = "Berlin"
|
||||||
|
for bez in bezirk_patterns:
|
||||||
|
if bez.lower() in row_text.lower():
|
||||||
|
location = bez
|
||||||
|
break
|
||||||
|
|
||||||
|
# Make absolute URL
|
||||||
|
if not href.startswith("http"):
|
||||||
|
href = f"http://www.wgcompany.de{href}" if href.startswith("/") else f"http://www.wgcompany.de/cgi-bin/{href}"
|
||||||
|
|
||||||
|
# Generate unique ID from link and key details
|
||||||
|
listing_id = hashlib.md5(f"{href}{price}{size}".encode()).hexdigest()[:12]
|
||||||
|
|
||||||
|
listings.append({
|
||||||
|
"id": listing_id,
|
||||||
|
"rooms": "1 Zimmer (WG)",
|
||||||
|
"size": size,
|
||||||
|
"price": price,
|
||||||
|
"address": location,
|
||||||
|
"link": href,
|
||||||
|
"source": "wgcompany",
|
||||||
|
"fetched_at": datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"[WGCOMPANY] Error parsing listing: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Deduplicate by id
|
||||||
|
seen_ids = set()
|
||||||
|
unique_listings = []
|
||||||
|
for listing in listings:
|
||||||
|
if listing["id"] not in seen_ids:
|
||||||
|
seen_ids.add(listing["id"])
|
||||||
|
unique_listings.append(listing)
|
||||||
|
listings = unique_listings
|
||||||
|
|
||||||
|
await page.close()
|
||||||
|
logger.info(f"[WGCOMPANY] Fetched {len(listings)} unique listings")
|
||||||
|
return listings
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[WGCOMPANY] Error fetching listings: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return []
|
||||||
|
|
||||||
|
def load_previous_listings(self) -> dict:
|
||||||
|
"""Load previously saved WGcompany listings"""
|
||||||
|
if WGCOMPANY_LISTINGS_FILE.exists():
|
||||||
|
with open(WGCOMPANY_LISTINGS_FILE, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_listings(self, listings: list[dict]):
|
||||||
|
"""Save current WGcompany listings"""
|
||||||
|
listings_dict = {l["id"]: l for l in listings}
|
||||||
|
with open(WGCOMPANY_LISTINGS_FILE, "w") as f:
|
||||||
|
json.dump(listings_dict, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
def find_new_listings(self, current: list[dict], previous: dict) -> list[dict]:
|
||||||
|
"""Find listings that are new since last check"""
|
||||||
|
new = []
|
||||||
|
for listing in current:
|
||||||
|
if listing["id"] not in previous:
|
||||||
|
new.append(listing)
|
||||||
|
return new
|
||||||
|
|
||||||
|
def send_telegram(self, message: str):
|
||||||
|
"""Send notification via Telegram"""
|
||||||
|
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
|
||||||
|
logger.warning("[WGCOMPANY] Telegram not configured, skipping notification")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
|
||||||
|
data = {
|
||||||
|
"chat_id": TELEGRAM_CHAT_ID,
|
||||||
|
"text": message,
|
||||||
|
"parse_mode": "HTML",
|
||||||
|
"disable_web_page_preview": True
|
||||||
|
}
|
||||||
|
response = requests.post(url, data=data)
|
||||||
|
if response.ok:
|
||||||
|
logger.info("[WGCOMPANY] Telegram notification sent")
|
||||||
|
else:
|
||||||
|
logger.error(f"[WGCOMPANY] Telegram error: {response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[WGCOMPANY] Telegram error: {e}")
|
||||||
|
|
||||||
|
def log_listing_times(self, new_listings: list[dict]):
|
||||||
|
"""Log new WGcompany listing appearance times to CSV"""
|
||||||
|
if not new_listings:
|
||||||
|
return
|
||||||
|
|
||||||
|
file_exists = WGCOMPANY_TIMING_FILE.exists()
|
||||||
|
|
||||||
|
with open(WGCOMPANY_TIMING_FILE, "a", newline="", encoding="utf-8") as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
if not file_exists:
|
||||||
|
writer.writerow(["timestamp", "weekday", "hour", "minute", "rooms", "size", "price", "address", "listing_id"])
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
for listing in new_listings:
|
||||||
|
writer.writerow([
|
||||||
|
now.isoformat(),
|
||||||
|
now.strftime("%A"),
|
||||||
|
now.hour,
|
||||||
|
now.minute,
|
||||||
|
listing["rooms"],
|
||||||
|
listing["size"],
|
||||||
|
listing["price"],
|
||||||
|
listing["address"],
|
||||||
|
listing["id"]
|
||||||
|
])
|
||||||
|
|
||||||
|
logger.info(f"[WGCOMPANY] Logged {len(new_listings)} listing times to CSV")
|
||||||
|
|
||||||
|
def notify_new_listings(self, new_listings: list[dict]):
|
||||||
|
"""Send individual notification for each new WGcompany listing"""
|
||||||
|
if not new_listings:
|
||||||
|
return
|
||||||
|
|
||||||
|
for listing in new_listings:
|
||||||
|
message = f"🏠 <b>Neues WG-Zimmer!</b> (WGcompany)\n\n"
|
||||||
|
message += f"🚪 <b>{listing['rooms']}</b>\n"
|
||||||
|
message += f"📐 {listing['size']}\n"
|
||||||
|
message += f"💰 {listing['price']}\n"
|
||||||
|
message += f"📍 {listing['address']}\n\n"
|
||||||
|
message += f"👉 <a href=\"{listing['link']}\">Zum Angebot</a>"
|
||||||
|
|
||||||
|
self.send_telegram(message)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
"""Run a single check for new WGcompany listings"""
|
||||||
|
logger.info("[WGCOMPANY] Starting check...")
|
||||||
|
|
||||||
|
# Fetch current listings
|
||||||
|
current_listings = asyncio.get_event_loop().run_until_complete(self._async_fetch())
|
||||||
|
if not current_listings:
|
||||||
|
logger.warning("[WGCOMPANY] No listings fetched")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Load previous listings
|
||||||
|
previous_listings = self.load_previous_listings()
|
||||||
|
|
||||||
|
# First run - just save baseline
|
||||||
|
if not previous_listings:
|
||||||
|
logger.info(f"[WGCOMPANY] First run - saving {len(current_listings)} listings as baseline")
|
||||||
|
self.save_listings(current_listings)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find new listings
|
||||||
|
new_listings = self.find_new_listings(current_listings, previous_listings)
|
||||||
|
|
||||||
|
if new_listings:
|
||||||
|
logger.info(f"[WGCOMPANY] Found {len(new_listings)} new listing(s)")
|
||||||
|
self.log_listing_times(new_listings)
|
||||||
|
self.notify_new_listings(new_listings)
|
||||||
|
else:
|
||||||
|
logger.info("[WGCOMPANY] No new listings")
|
||||||
|
|
||||||
|
# Save current state
|
||||||
|
self.save_listings(current_listings)
|
||||||
|
|
||||||
|
async def _async_fetch(self):
|
||||||
|
await self.init_browser()
|
||||||
|
return await self.fetch_listings()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main entry point"""
|
"""Main entry point"""
|
||||||
|
|
||||||
# Ensure data directory exists
|
# Ensure data directory exists
|
||||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
monitor = InBerlinMonitor()
|
# Initialize monitors
|
||||||
|
inberlin_monitor = InBerlinMonitor()
|
||||||
|
wgcompany_monitor = WGCompanyMonitor() if WGCOMPANY_ENABLED else None
|
||||||
|
|
||||||
# Start Telegram command listener
|
# Start Telegram command listener
|
||||||
telegram_bot = TelegramBot(monitor)
|
telegram_bot = TelegramBot(inberlin_monitor)
|
||||||
telegram_bot.start()
|
telegram_bot.start()
|
||||||
|
|
||||||
logger.info(f"inberlin-monitor started (interval: {CHECK_INTERVAL}s)")
|
logger.info(f"Monitor started (interval: {CHECK_INTERVAL}s)")
|
||||||
logger.info(f"Autopilot: {'ENABLED' if monitor.is_autopilot_enabled() else 'DISABLED'}")
|
logger.info(f"InBerlin Autopilot: {'ENABLED' if inberlin_monitor.is_autopilot_enabled() else 'DISABLED'}")
|
||||||
|
logger.info(f"WGcompany: {'ENABLED' if WGCOMPANY_ENABLED else 'DISABLED'}")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
# Check InBerlinWohnen
|
||||||
try:
|
try:
|
||||||
monitor.check()
|
inberlin_monitor.check()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Check failed: {e}")
|
logger.error(f"InBerlin check failed: {e}")
|
||||||
|
|
||||||
|
# Check WGcompany
|
||||||
|
if wgcompany_monitor:
|
||||||
|
try:
|
||||||
|
wgcompany_monitor.check()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"WGcompany check failed: {e}")
|
||||||
|
|
||||||
time.sleep(CHECK_INTERVAL)
|
time.sleep(CHECK_INTERVAL)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
requests>=2.31.0
|
requests>=2.31.0
|
||||||
playwright>=1.49.0
|
playwright>=1.57.0
|
||||||
matplotlib>=3.8.0
|
matplotlib>=3.8.0
|
||||||
pandas>=2.0.0
|
pandas>=2.0.0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue