fix emojis

This commit is contained in:
Aron Petau 2026-01-02 11:23:35 +01:00
parent c45c6992ae
commit 287b1b154f
5 changed files with 39 additions and 32 deletions

View file

@ -85,7 +85,7 @@ class ApplicationHandler:
company_label = company.capitalize() if company != "unknown" else "Wohnung" company_label = company.capitalize() if company != "unknown" else "Wohnung"
message = ( message = (
f"🏠 <b>[{company_label}] Neue Wohnung!</b>\n\n" f"<b>[{company_label}] Neue Wohnung!</b>\n\n"
f"🚪 <b>{listing['rooms']}</b>\n" f"🚪 <b>{listing['rooms']}</b>\n"
f"📏 {listing['size']}\n" f"📏 {listing['size']}\n"
f"💰 {listing['price']}\n" f"💰 {listing['price']}\n"
@ -136,7 +136,7 @@ class ApplicationHandler:
result = await self.apply(listing) result = await self.apply(listing)
results[listing["id"]] = result results[listing["id"]] = result
self.save_application(result) self.save_application(result)
status = "" if result["success"] else "" status = "[SUCCESS]" if result["success"] else "[FAILED]"
logger.info(f"{status} {listing['address'][:30]}... | {result['message'][:50]}") logger.info(f"{status} {listing['address'][:30]}... | {result['message'][:50]}")
await asyncio.sleep(2) await asyncio.sleep(2)
return results return results
@ -409,7 +409,7 @@ class ApplicationHandler:
else: else:
stats_text = "🎯 Peak time: N/A" stats_text = "🎯 Peak time: N/A"
stats_text = f"""📊 Summary Statistics stats_text = f"""<b>Summary Statistics</b>
Total listings tracked: {total_listings} Total listings tracked: {total_listings}
@ -632,15 +632,15 @@ Total listings tracked: {total_listings}
try: try:
listings = await self._fetch_listings_attempt() listings = await self._fetch_listings_attempt()
if attempt > 0: if attempt > 0:
logger.info(f"Fetch succeeded (attempt {attempt + 1})") logger.info(f"Fetch succeeded (attempt {attempt + 1})")
return listings return listings
except Exception as e: except Exception as e:
if attempt < max_retries - 1: if attempt < max_retries - 1:
wait_time = retry_delay * (2 ** attempt) # Exponential backoff wait_time = retry_delay * (2 ** attempt) # Exponential backoff
logger.warning(f"⚠️ Fetch failed (attempt {attempt + 1}/{max_retries}): {str(e)[:50]}... Retrying in {wait_time}s") logger.warning(f"Fetch failed (attempt {attempt + 1}/{max_retries}): {str(e)[:50]}... Retrying in {wait_time}s")
await asyncio.sleep(wait_time) await asyncio.sleep(wait_time)
else: else:
logger.error(f"Fetch failed after {max_retries} attempts") logger.error(f"Fetch failed after {max_retries} attempts")
return [] return []
return [] return []
@ -782,14 +782,14 @@ Total listings tracked: {total_listings}
listings = unique_listings listings = unique_listings
if not listings: if not listings:
logger.warning("⚠️ No listings parsed") logger.warning("No listings parsed")
await page.close() await page.close()
logger.info(f"📊 Fetched {len(listings)} listings") logger.info(f"Fetched {len(listings)} listings")
return listings return listings
except Exception as e: except Exception as e:
logger.error(f"Fetch error: {str(e)[:100]}") logger.error(f"Fetch error: {str(e)[:100]}")
return [] return []

View file

@ -92,6 +92,8 @@ class WBMHandler(BaseHandler):
# Look for application button on detail page # Look for application button on detail page
logger.info("[WBM] Looking for application button on detail page...") logger.info("[WBM] Looking for application button on detail page...")
selectors = [ selectors = [
'button:has-text("Anfrage absenden")',
'a:has-text("Anfrage absenden")',
'a[href*="expose-anfordern"]', 'a[href*="expose-anfordern"]',
'a[href*="bewerben"]', 'a[href*="bewerben"]',
'a:has-text("Anfragen")', 'a:has-text("Anfragen")',

View file

@ -151,7 +151,7 @@ class WGCompanyNotifier:
if listing['id'] not in previous: if listing['id'] not in previous:
new.append(listing) new.append(listing)
if new: if new:
logger.info(f"[WG] 🏠 {len(new)} new listing{'s' if len(new) > 1 else ''} detected") logger.info(f"[WG] {len(new)} new listing{'s' if len(new) > 1 else ''} detected")
return new return new
def log_listing_times(self, new_listings): def log_listing_times(self, new_listings):
@ -185,7 +185,7 @@ class WGCompanyNotifier:
for idx, listing in enumerate(new_listings, start=1): for idx, listing in enumerate(new_listings, start=1):
try: try:
message = ( message = (
f"🏠 <b>[WG-Company] Neues WG-Zimmer!</b>\n\n" f"<b>[WG-Company] Neues WG-Zimmer!</b>\n\n"
f"🚪 <b>{listing['rooms']}</b>\n" f"🚪 <b>{listing['rooms']}</b>\n"
f"📏 {listing['size']}\n" f"📏 {listing['size']}\n"
f"💰 {listing['price']}\n" f"💰 {listing['price']}\n"
@ -196,7 +196,7 @@ class WGCompanyNotifier:
asyncio.run_coroutine_threadsafe(self.telegram_bot._send_message(message), loop) asyncio.run_coroutine_threadsafe(self.telegram_bot._send_message(message), loop)
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
except Exception as e: except Exception as e:
logger.error(f"[WG] Telegram failed for listing {idx}: {str(e)[:50]}") logger.error(f"[WG] Telegram failed for listing {idx}: {str(e)[:50]}")
async def run(self): async def run(self):
await self.init_browser() await self.init_browser()

23
main.py
View file

@ -1,4 +1,3 @@
import asyncio import asyncio
from playwright.async_api import async_playwright from playwright.async_api import async_playwright
from application_handler import ApplicationHandler from application_handler import ApplicationHandler
@ -22,13 +21,13 @@ logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format="%(asctime)s [%(levelname)-5s] %(name)-20s | %(message)s", format="%(asctime)s [%(levelname)-5s] %(name)-20s | %(message)s",
handlers=[ handlers=[
RotatingFileHandler("data/monitor.log", maxBytes=1 * 1024 * 1024, backupCount=3), # 1 MB per file, 3 backups RotatingFileHandler("data/monitor.log", maxBytes=1 * 1024 * 1024, backupCount=3),
logging.StreamHandler() logging.StreamHandler()
], ],
force=True # Enforce for all modules, Python 3.8+ force=True # Enforce for all modules, Python 3.8+
) )
logger = logging.getLogger(__name__) # Use named logger logger = logging.getLogger(__name__) # Use named logger
logger.info("🚀 Bot starting | Logs: data/monitor.log + console") logger.info("Bot starting | Logs: data/monitor.log + console")
# Interval (seconds) between checks for new listings # Interval (seconds) between checks for new listings
CHECK_INTERVAL = int(os.getenv("CHECK_INTERVAL", 300)) # Default: 300 seconds CHECK_INTERVAL = int(os.getenv("CHECK_INTERVAL", 300)) # Default: 300 seconds
@ -96,7 +95,7 @@ async def init_browser_context() -> tuple:
return playwright, browser, browser_context return playwright, browser, browser_context
async def main() -> None: async def main() -> None:
logger.info("🤖 Initializing wohn-bot...") logger.info("Initializing wohn-bot...")
# Validate configuration before starting # Validate configuration before starting
if not validate_config(): if not validate_config():
@ -106,7 +105,7 @@ async def main() -> None:
state_manager = StateManager(Path("data/state.json")) state_manager = StateManager(Path("data/state.json"))
# --- Playwright browser/context setup with recovery --- # --- Playwright browser/context setup with recovery ---
logger.info("🌐 Initializing browser...") logger.info("Initializing browser...")
playwright, browser, browser_context = await init_browser_context() playwright, browser, browser_context = await init_browser_context()
# Application handler manages browser/context # Application handler manages browser/context
@ -134,16 +133,16 @@ async def main() -> None:
try: try:
deleted = autoclean_debug_material() deleted = autoclean_debug_material()
if deleted: if deleted:
logger.info(f"🧹 Cleaned {len(deleted)} debug files (48h)") logger.info(f"Cleaned {len(deleted)} debug files (48h)")
except Exception as e: except Exception as e:
logger.warning(f"⚠️ Autoclean failed: {e}") logger.warning(f"Autoclean failed: {e}")
last_clean = now last_clean = now
try: try:
current_listings = await app_handler.fetch_listings() current_listings = await app_handler.fetch_listings()
except Exception as e: except Exception as e:
logger.error(f"💥 Browser crash: {e}") logger.error(f"💥 Browser crash: {e}")
logger.info("🔄 Recovering...") logger.info("Recovering...")
try: try:
await browser.close() await browser.close()
await playwright.stop() await playwright.stop()
@ -154,7 +153,7 @@ async def main() -> None:
try: try:
playwright, browser, browser_context = await init_browser_context() playwright, browser, browser_context = await init_browser_context()
app_handler.context = browser_context app_handler.context = browser_context
logger.info("Browser recovered") logger.info("Browser recovered")
await asyncio.sleep(5) await asyncio.sleep(5)
continue continue
except Exception as recovery_error: except Exception as recovery_error:
@ -163,7 +162,7 @@ async def main() -> None:
continue continue
if not current_listings: if not current_listings:
logger.warning("⚠️ No listings fetched") logger.warning("No listings fetched")
await asyncio.sleep(CHECK_INTERVAL) await asyncio.sleep(CHECK_INTERVAL)
_flush_rotating_file_handlers() _flush_rotating_file_handlers()
continue continue
@ -192,10 +191,10 @@ async def main() -> None:
new_listings = app_handler.find_new_listings(current_listings, previous_listings) new_listings = app_handler.find_new_listings(current_listings, previous_listings)
application_results = {} application_results = {}
if new_listings: if new_listings:
logger.info(f"\ud83c\udfe0 {len(new_listings)} new listing{'s' if len(new_listings) > 1 else ''} detected") logger.info(f"{len(new_listings)} new listing{'s' if len(new_listings) > 1 else ''} detected")
app_handler.log_listing_times(new_listings) app_handler.log_listing_times(new_listings)
if app_handler.is_autopilot_enabled(): if app_handler.is_autopilot_enabled():
logger.info("\ud83e\udd16 Autopilot active - applying...") logger.info("Autopilot active - applying...")
application_results = await app_handler.apply_to_listings(new_listings) application_results = await app_handler.apply_to_listings(new_listings)
app_handler.notify_new_listings(new_listings, application_results) app_handler.notify_new_listings(new_listings, application_results)
app_handler.save_listings(current_listings) app_handler.save_listings(current_listings)

View file

@ -62,7 +62,7 @@ class TelegramBot:
await self._send_message(msg) await self._send_message(msg)
except Exception as e: except Exception as e:
logger.error(f"Error resetting listings: {e}") logger.error(f"Error resetting listings: {e}")
await self._send_message(f" Error resetting listings: {str(e)}") await self._send_message(f"[ERROR] Error resetting listings: {str(e)}")
def __init__(self, monitor, bot_token: str | None = None, chat_id: str | None = None, event_loop=None) -> None: def __init__(self, monitor, bot_token: str | None = None, chat_id: str | None = None, event_loop=None) -> None:
self.monitor = monitor self.monitor = monitor
@ -174,9 +174,9 @@ class TelegramBot:
and app.get("retries", 0) < max_retries and app.get("retries", 0) < max_retries
and not app.get("deactivated", False) and not app.get("deactivated", False)
] ]
await self._send_message(f"🔄 Retrying {len(failed)} failed applications (max retries: {max_retries})...") await self._send_message(f"[RETRY] Retrying {len(failed)} failed applications (max retries: {max_retries})...")
if not failed: if not failed:
await self._send_message(" No failed applications to retry (or all reached max retries).") await self._send_message("[INFO] No failed applications to retry (or all reached max retries).")
return return
results = {} results = {}
details = [] details = []
@ -197,7 +197,7 @@ class TelegramBot:
result["timestamp"] = app.get("timestamp", result["timestamp"]) result["timestamp"] = app.get("timestamp", result["timestamp"])
self.app_handler.save_application(result) self.app_handler.save_application(result)
results[listing["id"]] = result results[listing["id"]] = result
status_emoji = "" if result["success"] else "" status_emoji = "[SUCCESS]" if result["success"] else "[FAILED]"
details.append( details.append(
f"{status_emoji} <b>{result.get('address', '')}</b> ({result.get('company', '')})\n" f"{status_emoji} <b>{result.get('address', '')}</b> ({result.get('company', '')})\n"
f"<code>{result.get('link', '')}</code>\n" f"<code>{result.get('link', '')}</code>\n"
@ -205,7 +205,7 @@ class TelegramBot:
) )
n_success = sum(1 for r in results.values() if r["success"]) n_success = sum(1 for r in results.values() if r["success"])
n_fail = sum(1 for r in results.values() if not r["success"]) n_fail = sum(1 for r in results.values() if not r["success"])
summary = f"🔄 Retried {len(results)} failed applications.\n✅ Success: {n_success}\n❌ Still failed: {n_fail}" summary = f"[RETRY] Retried {len(results)} failed applications.\n[SUCCESS]: {n_success}\n[FAILED]: {n_fail}"
if details: if details:
summary += "\n\n<b>Details:</b>\n" + "\n".join(details) summary += "\n\n<b>Details:</b>\n" + "\n".join(details)
await self._send_message(summary) await self._send_message(summary)
@ -220,7 +220,7 @@ class TelegramBot:
if action == "on": if action == "on":
logger.info("Enabling autopilot mode") logger.info("Enabling autopilot mode")
self.monitor.set_autopilot(True) self.monitor.set_autopilot(True)
await self._send_message("🤖 <b>Autopilot ENABLED</b>\n\nI will automatically apply to new listings!") await self._send_message("<b>Autopilot ENABLED</b>\n\nI will automatically apply to new listings!")
elif action == "off": elif action == "off":
self.monitor.set_autopilot(False) self.monitor.set_autopilot(False)
await self._send_message("🛑 <b>Autopilot DISABLED</b>\n\nI will only notify you of new listings.") await self._send_message("🛑 <b>Autopilot DISABLED</b>\n\nI will only notify you of new listings.")
@ -231,7 +231,7 @@ class TelegramBot:
state = self.app_handler.load_state() state = self.app_handler.load_state()
autopilot = state.get("autopilot", False) autopilot = state.get("autopilot", False)
applications = self.app_handler.load_applications() applications = self.app_handler.load_applications()
status = "🤖 <b>Autopilot:</b> " + ("ON" if autopilot else "OFF") status = "<b>Autopilot:</b> " + ("ON" if autopilot else "OFF")
status += f"\n📝 <b>Applications sent:</b> {len(applications)}" status += f"\n📝 <b>Applications sent:</b> {len(applications)}"
by_company: dict[str, int] = {} by_company: dict[str, int] = {}
for app in applications.values(): for app in applications.values():
@ -264,7 +264,7 @@ class TelegramBot:
logger.error(f"Error generating errorrate plot: {e}") logger.error(f"Error generating errorrate plot: {e}")
import traceback import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
await self._send_message(f" Error generating errorrate plot: {str(e)}") await self._send_message(f"[ERROR] Error generating errorrate plot: {str(e)}")
async def _send_message(self, text: str) -> None: async def _send_message(self, text: str) -> None:
@ -274,6 +274,9 @@ class TelegramBot:
logger.warning("Telegram bot token or chat ID not configured, cannot send message") logger.warning("Telegram bot token or chat ID not configured, cannot send message")
return return
# Clean text: remove invalid unicode surrogates
text = text.encode('utf-8', errors='ignore').decode('utf-8')
url = f"https://api.telegram.org/bot{self.bot_token}/sendMessage" url = f"https://api.telegram.org/bot{self.bot_token}/sendMessage"
# Split message into chunks if too long # Split message into chunks if too long
@ -340,6 +343,9 @@ class TelegramBot:
logger.warning("Telegram bot token or chat ID not configured, cannot send photo") logger.warning("Telegram bot token or chat ID not configured, cannot send photo")
return return
# Clean caption: remove invalid unicode surrogates
caption = caption.encode('utf-8', errors='ignore').decode('utf-8')
url = f"https://api.telegram.org/bot{self.bot_token}/sendPhoto" url = f"https://api.telegram.org/bot{self.bot_token}/sendPhoto"
max_retries = 3 max_retries = 3
retry_delay = 1 # Initial delay in seconds retry_delay = 1 # Initial delay in seconds