fix emojis
This commit is contained in:
parent
c45c6992ae
commit
287b1b154f
5 changed files with 39 additions and 32 deletions
|
|
@ -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 []
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")',
|
||||||
|
|
|
||||||
|
|
@ -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
23
main.py
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue