mostly working shape?
This commit is contained in:
parent
3057cda8d3
commit
540a3cc884
10 changed files with 462 additions and 183 deletions
191
telegram_bot.py
191
telegram_bot.py
|
|
@ -1,4 +1,5 @@
|
|||
|
||||
|
||||
import os
|
||||
import logging
|
||||
import threading
|
||||
|
|
@ -7,6 +8,7 @@ import requests
|
|||
import asyncio
|
||||
|
||||
|
||||
|
||||
# Configuration from environment
|
||||
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
||||
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "")
|
||||
|
|
@ -16,9 +18,30 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class TelegramBot:
|
||||
"""Handle Telegram commands for controlling the monitor"""
|
||||
|
||||
def _handle_reset_listings_command(self):
|
||||
async def _handle_help_command(self):
|
||||
"""Send a help message with available commands."""
|
||||
help_text = (
|
||||
"<b>Available commands:</b>\n"
|
||||
"/autopilot on|off - Enable/disable autopilot\n"
|
||||
"/status - Show current status\n"
|
||||
"/plot - Show weekly listing pattern plot\n"
|
||||
"/errorrate - Show autopilot error rate plot\n"
|
||||
"/retryfailed - Retry failed applications\n"
|
||||
"/resetlistings - Reset listings file\n"
|
||||
"/help - Show this help message"
|
||||
)
|
||||
await self._send_message(help_text)
|
||||
|
||||
async def _handle_unknown_command(self, text):
|
||||
"""Handle unknown commands and notify the user."""
|
||||
cmd = text.split()[0] if text else text
|
||||
msg = (
|
||||
f"❓ Unknown command: <code>{cmd}</code>\n\nUse /help to see available commands."
|
||||
)
|
||||
await self._send_message(msg)
|
||||
|
||||
async def _handle_reset_listings_command(self):
|
||||
"""Move listings.json to data/old/ with a timestamp, preserving statistics and application history."""
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
|
@ -29,15 +52,20 @@ class TelegramBot:
|
|||
# Ensure old_dir exists
|
||||
os.makedirs(old_dir, exist_ok=True)
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
dest_path = os.path.join(old_dir, f"listings_{timestamp}.json")
|
||||
dest_path = os.path.join(
|
||||
old_dir, f"listings_{timestamp}.json"
|
||||
)
|
||||
shutil.move(listings_path, dest_path)
|
||||
msg = f"🗑️ <b>Listings reset:</b>\n<code>listings.json</code> moved to <code>old/listings_{timestamp}.json</code>."
|
||||
msg = (
|
||||
f"🗑️ <b>Listings reset:</b>\n<code>listings.json</code> moved to "
|
||||
f"<code>old/listings_{timestamp}.json</code>."
|
||||
)
|
||||
else:
|
||||
msg = "ℹ️ No listings file found to move."
|
||||
self._send_message(msg)
|
||||
await self._send_message(msg)
|
||||
except Exception as e:
|
||||
logger.error(f"Error resetting listings: {e}")
|
||||
self._send_message(f"❌ Error resetting listings: {str(e)}")
|
||||
await self._send_message(f"❌ Error resetting listings: {str(e)}")
|
||||
|
||||
def __init__(self, monitor, bot_token=None, chat_id=None, event_loop=None):
|
||||
self.monitor = monitor
|
||||
|
|
@ -89,44 +117,31 @@ class TelegramBot:
|
|||
logger.debug(f"Ignoring message from unknown chat: {chat_id}")
|
||||
return
|
||||
logger.info(f"Received Telegram command: {text}")
|
||||
loop = self.event_loop
|
||||
if text.startswith("/autopilot"):
|
||||
self._handle_autopilot_command(text)
|
||||
asyncio.run_coroutine_threadsafe(self._handle_autopilot_command(text), loop)
|
||||
elif text == "/status":
|
||||
self._handle_status_command()
|
||||
asyncio.run_coroutine_threadsafe(self._handle_status_command(), loop)
|
||||
elif text == "/help":
|
||||
self._handle_help_command()
|
||||
asyncio.run_coroutine_threadsafe(self._handle_help_command(), loop)
|
||||
elif text == "/plot":
|
||||
self._handle_plot_command()
|
||||
asyncio.run_coroutine_threadsafe(self._handle_plot_command(), loop)
|
||||
elif text == "/errorrate":
|
||||
self._handle_error_rate_command()
|
||||
asyncio.run_coroutine_threadsafe(self._handle_error_rate_command(), loop)
|
||||
elif text == "/retryfailed":
|
||||
# Schedule coroutine on the main event loop for thread safety
|
||||
fut = asyncio.run_coroutine_threadsafe(
|
||||
self._handle_retry_failed_command(max_retries=TELEGRAM_MAX_RETRIES),
|
||||
self.event_loop
|
||||
loop
|
||||
)
|
||||
# Optionally, wait for result or handle exceptions
|
||||
try:
|
||||
fut.result()
|
||||
except Exception as e:
|
||||
logger.error(f"/retryfailed command failed: {e}")
|
||||
elif text == "/resetlistings":
|
||||
self._handle_reset_listings_command()
|
||||
asyncio.run_coroutine_threadsafe(self._handle_reset_listings_command(), loop)
|
||||
elif text.startswith("/"):
|
||||
self._handle_unknown_command(text)
|
||||
def _handle_reset_listings_command(self):
|
||||
"""Delete listings.json (not wgcompany_listings.json), but preserve statistics and application history."""
|
||||
try:
|
||||
listings_path = os.path.join("data", "listings.json")
|
||||
if os.path.exists(listings_path):
|
||||
os.remove(listings_path)
|
||||
msg = "🗑️ <b>Listings reset:</b>\n<code>listings.json</code> deleted."
|
||||
else:
|
||||
msg = "ℹ️ No listings file found to delete."
|
||||
self._send_message(msg)
|
||||
except Exception as e:
|
||||
logger.error(f"Error resetting listings: {e}")
|
||||
self._send_message(f"❌ Error resetting listings: {str(e)}")
|
||||
asyncio.run_coroutine_threadsafe(self._handle_unknown_command(text), loop)
|
||||
|
||||
async def _handle_retry_failed_command(self, max_retries: int = 3):
|
||||
"""Retry all failed applications up to max_retries."""
|
||||
# Ensure browser context is initialized
|
||||
|
|
@ -137,11 +152,11 @@ class TelegramBot:
|
|||
if hasattr(self.app_handler, 'context') and hasattr(self.app_handler, 'handlers'):
|
||||
for handler in self.app_handler.handlers.values():
|
||||
handler.context = self.app_handler.context
|
||||
self._send_message(f"🔄 Retrying failed applications (max retries: {max_retries})...")
|
||||
applications = self.app_handler.load_applications()
|
||||
failed = [app for app in applications.values() if not app.get("success") and app.get("retries", 0) < max_retries]
|
||||
await self._send_message(f"🔄 Retrying {len(failed)} failed applications (max retries: {max_retries})...")
|
||||
if not failed:
|
||||
self._send_message("✅ No failed applications to retry (or all reached max retries).")
|
||||
await self._send_message("✅ No failed applications to retry (or all reached max retries).")
|
||||
return
|
||||
results = {}
|
||||
details = []
|
||||
|
|
@ -170,26 +185,26 @@ class TelegramBot:
|
|||
summary = f"🔄 Retried {len(results)} failed applications.\n✅ Success: {n_success}\n❌ Still failed: {n_fail}"
|
||||
if details:
|
||||
summary += "\n\n<b>Details:</b>\n" + "\n".join(details)
|
||||
self._send_message(summary)
|
||||
await self._send_message(summary)
|
||||
|
||||
def _handle_autopilot_command(self, text):
|
||||
async def _handle_autopilot_command(self, text):
|
||||
logger.info(f"Processing autopilot command: {text}")
|
||||
parts = text.split()
|
||||
if len(parts) < 2:
|
||||
self._send_message("Usage: /autopilot on|off")
|
||||
await self._send_message("Usage: /autopilot on|off")
|
||||
return
|
||||
action = parts[1].lower()
|
||||
if action == "on":
|
||||
logger.info("Enabling autopilot mode")
|
||||
self.monitor.set_autopilot(True)
|
||||
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":
|
||||
self.monitor.set_autopilot(False)
|
||||
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.")
|
||||
else:
|
||||
self._send_message("Usage: /autopilot on|off")
|
||||
await self._send_message("Usage: /autopilot on|off")
|
||||
|
||||
def _handle_status_command(self):
|
||||
async def _handle_status_command(self):
|
||||
state = self.app_handler.load_state()
|
||||
autopilot = state.get("autopilot", False)
|
||||
applications = self.app_handler.load_applications()
|
||||
|
|
@ -203,89 +218,83 @@ class TelegramBot:
|
|||
status += "\n\n<b>By company:</b>"
|
||||
for company, count in sorted(by_company.items()):
|
||||
status += f"\n • {company}: {count}"
|
||||
self._send_message(status)
|
||||
await self._send_message(status)
|
||||
|
||||
def _handle_plot_command(self):
|
||||
async def _handle_plot_command(self):
|
||||
logger.info("Generating listing times plot...")
|
||||
try:
|
||||
plot_path = self.app_handler._generate_weekly_plot()
|
||||
self._send_photo(plot_path, "\U0001f4ca <b>Weekly Listing Patterns</b>\n\nThis shows when new listings typically appear throughout the week.")
|
||||
await self._send_photo(plot_path, "\U0001f4ca <b>Weekly Listing Patterns</b>\n\nThis shows when new listings typically appear throughout the week.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating plot: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
self._send_message(f"\u274c Error generating plot: {str(e)}")
|
||||
cmd = text.split()[0] if text else text
|
||||
self._send_message(f"❓ Unknown command: <code>{cmd}</code>\n\nUse /help to see available commands.")
|
||||
await self._send_message(f"\u274c Error generating plot: {str(e)}")
|
||||
|
||||
def _handle_error_rate_command(self):
|
||||
async def _handle_error_rate_command(self):
|
||||
logger.info("Generating autopilot errorrate plot...")
|
||||
try:
|
||||
plot_path, summary = self.app_handler._generate_error_rate_plot()
|
||||
caption = "📉 <b>Autopilot Success vs Failure</b>\n\n" + summary
|
||||
self._send_photo(plot_path, caption)
|
||||
await self._send_photo(plot_path, caption)
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating errorrate plot: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
self._send_message(f"❌ Error generating errorrate plot: {str(e)}")
|
||||
await self._send_message(f"❌ Error generating errorrate plot: {str(e)}")
|
||||
|
||||
def _handle_plot_command(self):
|
||||
logger.info("Generating listing times plot...")
|
||||
try:
|
||||
plot_path = self.app_handler._generate_weekly_plot()
|
||||
if plot_path:
|
||||
self._send_photo(plot_path, "📊 <b>Weekly Listing Patterns</b>\n\nThis shows when new listings typically appear throughout the week.")
|
||||
else:
|
||||
self._send_message("📊 Not enough data to generate plot yet. Keep monitoring!")
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating plot: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
self._send_message(f"❌ Error generating plot: {str(e)}")
|
||||
|
||||
def _send_message(self, text):
|
||||
"""Send a text message to the configured Telegram chat, with detailed error logging."""
|
||||
async def _send_message(self, text):
|
||||
"""Send a text message to the configured Telegram chat, with detailed error logging (async)."""
|
||||
import httpx
|
||||
MAX_LENGTH = 4096 # Telegram message character limit
|
||||
if not self.bot_token or not self.chat_id:
|
||||
logger.warning("Telegram bot token or chat ID not configured, cannot send message")
|
||||
return
|
||||
url = f"https://api.telegram.org/bot{self.bot_token}/sendMessage"
|
||||
payload = {"chat_id": self.chat_id, "text": text, "parse_mode": "HTML"}
|
||||
# Split message into chunks if too long
|
||||
messages = []
|
||||
if isinstance(text, str) and len(text) > MAX_LENGTH:
|
||||
# Try to split on line breaks for readability
|
||||
lines = text.split('\n')
|
||||
chunk = ""
|
||||
for line in lines:
|
||||
if len(chunk) + len(line) + 1 > MAX_LENGTH:
|
||||
messages.append(chunk)
|
||||
chunk = line
|
||||
else:
|
||||
if chunk:
|
||||
chunk += "\n"
|
||||
chunk += line
|
||||
if chunk:
|
||||
messages.append(chunk)
|
||||
else:
|
||||
messages = [text]
|
||||
try:
|
||||
response = requests.post(url, json=payload, timeout=10)
|
||||
logger.info(f"[TELEGRAM] Sent message: status={response.status_code}, ok={response.ok}, response={response.text}")
|
||||
if not response.ok:
|
||||
logger.error(f"Failed to send Telegram message: {response.text}")
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
for idx, msg in enumerate(messages):
|
||||
payload = {"chat_id": self.chat_id, "text": msg, "parse_mode": "HTML"}
|
||||
response = await client.post(url, json=payload)
|
||||
logger.info(f"[TELEGRAM] Sent message part {idx+1}/{len(messages)}: status={response.status_code}, ok={response.is_success}")
|
||||
if not response.is_success:
|
||||
logger.error(f"Failed to send Telegram message: {response.text}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error while sending Telegram message: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
def _send_photo(self, photo_path, caption):
|
||||
"""Send a photo to the configured Telegram chat."""
|
||||
async def _send_photo(self, photo_path, caption):
|
||||
"""Send a photo to the configured Telegram chat (async)."""
|
||||
import httpx
|
||||
if not self.bot_token or not self.chat_id:
|
||||
logger.warning("Telegram bot token or chat ID not configured, cannot send photo")
|
||||
return
|
||||
url = f"https://api.telegram.org/bot{self.bot_token}/sendPhoto"
|
||||
with open(photo_path, "rb") as photo:
|
||||
payload = {"chat_id": self.chat_id, "caption": caption, "parse_mode": "HTML"}
|
||||
files = {"photo": photo}
|
||||
try:
|
||||
response = requests.post(url, data=payload, files=files, timeout=10)
|
||||
if not response.ok:
|
||||
logger.error(f"Failed to send Telegram photo: {response.text}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error while sending Telegram photo: {e}")
|
||||
|
||||
def _generate_error_rate_plot(self):
|
||||
"""Generate and send a plot showing success vs failure ratio for autopilot applications."""
|
||||
logger.info("Generating autopilot errorrate plot...")
|
||||
try:
|
||||
plot_path, summary = self.app_handler._generate_error_rate_plot()
|
||||
if plot_path:
|
||||
self._send_photo(plot_path, caption=summary)
|
||||
else:
|
||||
self._send_message("No data available to generate the error rate plot.")
|
||||
with open(photo_path, "rb") as photo:
|
||||
files = {"photo": (photo_path, photo, "image/jpeg")}
|
||||
data = {"chat_id": self.chat_id, "caption": caption, "parse_mode": "HTML"}
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
response = await client.post(url, data=data, files=files)
|
||||
if not response.is_success:
|
||||
logger.error(f"Failed to send Telegram photo: {response.text}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating errorrate plot: {e}")
|
||||
self._send_message(f"❌ Error generating errorrate plot: {str(e)}")
|
||||
logger.error(f"Error while sending Telegram photo: {e}")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue