/
zimaaadev
/
Parser
Обзор
Документация
Войти
/
zimaaadev
/
Parser
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
bot/bot.py
435 строк
17 KB
zimaaadev
Ищет природные объекты, набнрежные и т.п.
24 дек 2025, 17:18
24 дек 2025, 17:18
2d73bc2
Код
Авторство
О чём код?
import logging import sys import os from typing import Dict, Any from bot.district_analyzer import analyze_district_with_gigachat from bot.utils.subscriptions import subscribe, unsubscribe from telegram.ext import ApplicationBuilder, JobQueue # Расширяем путь для импорта sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from telegram import Update, ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import ( ApplicationBuilder, CommandHandler, MessageHandler, ContextTypes, filters, ) from telegram.constants import ParseMode as TgParseMode import asyncio # Конфиг и зависимости from config import TELEGRAM_BOT_TOKEN from bot.utils.database import init_db # ensure init_db is here from bot.utils.geocoder import geocode_address from bot.utils.places import get_nearby_places # Импорт парсера from parsers.aggregator import aggregate_listings logger = logging.getLogger(__name__) logger.info("🧠 GigaChat LLM подключён — анализ района активирован") # === Глобальный контекст пользователей === user_context: Dict[int, Dict[str, Any]] = {} # --- Клавиатуры --- def get_menu_keyboard(): keyboard = [ [KeyboardButton("🔍 Посмотреть последние")], [KeyboardButton("✅ Подписаться"), KeyboardButton("🚫 Отписаться")], [KeyboardButton("📊 Статистика"), KeyboardButton("❓ Помощь")], [KeyboardButton("🏘 Инфо по адресу")], [KeyboardButton("🌐 Настройка парсинга")] ] return ReplyKeyboardMarkup(keyboard, resize_keyboard=True, input_field_placeholder="Выберите действие...") def get_sites_keyboard(): keyboard = [ [KeyboardButton("ЦИАН")], [KeyboardButton("Avito")], [KeyboardButton("Домофонд")], [KeyboardButton("Яндекс.Недвижимость")], [KeyboardButton("🔙 Назад")] ] return ReplyKeyboardMarkup(keyboard, resize_keyboard=True) def get_categories_keyboard(): keyboard = [ [KeyboardButton("Квартиры")], [KeyboardButton("Дома")], [KeyboardButton("Коммерческая")], [KeyboardButton("🔙 Назад")] ] return ReplyKeyboardMarkup(keyboard, resize_keyboard=True) # --- /start --- async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): user_id = update.effective_user.id user = update.effective_user first_name = user.first_name ctx = user_context.setdefault(user_id, {}) if ctx.get("first_time") is None: ctx["first_time"] = False ctx.update({ "site": None, "city": None, "category": None, "awaiting_city": False, "awaiting_category": False, "awaiting_address": False }) await update.message.reply_text( f"👋 Привет, {first_name}!\n\n" "🏠 Добро пожаловать в бот мониторинга недвижимости!\n\n" "Я помогу вам:\n" "• Следить за ценами на квартиры, дома и коммерческую недвижимость\n" "• Анализировать районы по адресу\n" "• Получать новые объявления в реальном времени\n\n" "Выберите действие в меню 👇", parse_mode=TgParseMode.HTML, reply_markup=get_menu_keyboard() ) else: await update.message.reply_text( f"С возвращением, {first_name}! 🎉\n\n" "Выберите действие в меню 👇", parse_mode=TgParseMode.HTML, reply_markup=get_menu_keyboard() ) # --- Инфо по адресу --- async def handle_info_request(update: Update, context: ContextTypes.DEFAULT_TYPE): user_id = update.effective_user.id text = update.message.text ctx = user_context.setdefault(user_id, {}) if text == "🏘 Инфо по адресу": ctx["awaiting_address"] = True await update.message.reply_text( "📬 Введите адрес текстом (например: Ленина 10, Майкоп)", reply_markup=ReplyKeyboardMarkup([[KeyboardButton("🔙 Назад")]], resize_keyboard=True) ) return if text == "🔙 Назад": ctx["awaiting_address"] = False await update.message.reply_text("Возврат в меню.", reply_markup=get_menu_keyboard()) return if ctx.get("awaiting_address"): address = text.strip() if len(address) < 5: await update.message.reply_text("❌ Введите более полный адрес (например: ул. Ленина, 10)") return # ✅ 1. Сообщение: "Ищу..." wait_msg = await update.message.reply_text( f"🔍 Ищу: <b>{address}</b>...\n\n" "🌍 Определяем координаты и собираем данные...", parse_mode=TgParseMode.HTML ) try: # ✅ Геокодинг: ИСПРАВЛЕНО — используем geocode_address coords = await geocode_address(address) if not coords: await context.bot.edit_message_text( chat_id=wait_msg.chat_id, message_id=wait_msg.message_id, text="❌ Адрес не найден. Проверьте написание." ) return lat, lon = coords # ✅ 2. Собираем объекты await context.bot.edit_message_text( chat_id=wait_msg.chat_id, message_id=wait_msg.message_id, text=f"🌍 Собираю данные о районе:\n• Транспорт\n• Магазины\n• Парки и набережные", parse_mode=TgParseMode.HTML ) city = address.split(",")[-1].strip() if "," in address else "городе" places = await get_nearby_places(lat, lon, city=city) if not places: await context.bot.edit_message_text( chat_id=wait_msg.chat_id, message_id=wait_msg.message_id, text="📭 Рядом нет значимых объектов." ) return # ✅ 3. Анализ await context.bot.edit_message_text( chat_id=wait_msg.chat_id, message_id=wait_msg.message_id, text="🧠 Генерирую анализ через GigaChat...", parse_mode=TgParseMode.HTML ) summary = await analyze_district_with_gigachat(address, places) # ✅ 4. Финальный результат yandex_map_url = f"https://yandex.ru/maps/?ll={lon}%2C{lat}&z=16&pt={lon},{lat}&l=map" result = ( f"📍 <b>Адрес:</b> {address}\n\n" f"🧠 <b>О районе:</b>\n{summary}\n\n" f"🔧 <b>Рядом:</b>\n" + "\n".join([f"• {p}" for p in places[:6]]) ) await context.bot.edit_message_text( chat_id=wait_msg.chat_id, message_id=wait_msg.message_id, text=result, parse_mode=TgParseMode.HTML ) # Карта keyboard = [[InlineKeyboardButton("📍 Открыть на Яндекс.Картах", url=yandex_map_url)]] await update.message.reply_text("🗺 Посмотреть на карте:", reply_markup=InlineKeyboardMarkup(keyboard)) # Сохранение ctx.update({ "address": address, "places": places, "lat": lat, "lon": lon }) ctx["awaiting_address"] = False except Exception as e: logger.error(f"Ошибка в handle_info_request: {e}") await context.bot.edit_message_text( chat_id=wait_msg.chat_id, message_id=wait_msg.message_id, text="❌ Произошла ошибка. Повторите позже." ) # --- Настройка парсинга --- async def handle_parsing_setup(update: Update, context: ContextTypes.DEFAULT_TYPE): user_id = update.effective_user.id text = update.message.text ctx = user_context.setdefault(user_id, {}) if text == "🌐 Настройка парсинга": ctx.update({ "site": None, "city": None, "category": None, "awaiting_city": False, "awaiting_category": False }) await update.message.reply_text( "🌐 Выберите сайт для парсинга:", reply_markup=get_sites_keyboard() ) return if text == "🔙 Назад": await update.message.reply_text("Возврат в меню.", reply_markup=get_menu_keyboard()) return if text in ["ЦИАН", "Avito", "Домофонд", "Яндекс.Недвижимость"]: ctx["site"] = text await update.message.reply_text( f"✅ Выбран сайт: <b>{text}</b>\n\nВведите город:", parse_mode=TgParseMode.HTML, reply_markup=ReplyKeyboardMarkup( [[KeyboardButton("Майкоп")], [KeyboardButton("🔙 Назад")]], resize_keyboard=True ) ) ctx["awaiting_city"] = True return # --- Посмотреть последние --- async def handle_last_listings(update: Update, context: ContextTypes.DEFAULT_TYPE): user_id = update.effective_user.id ctx = user_context.get(user_id, {}) site, city, category = ctx.get("site"), ctx.get("city"), ctx.get("category") if not all([site, city, category]): await update.message.reply_text( "❌ Настройте парсинг через «🌐 Настройка парсинга»", reply_markup=get_menu_keyboard() ) return await update.message.reply_text( f"🔍 Ищу {category.lower()} в {city} на {site}...\nПодождите немного 🕵️♂️", reply_markup=get_menu_keyboard() ) found = 0 try: async for item in aggregate_listings(): if item.get("source") == site: found += 1 message = ( f"📌 <b>{item['title']}</b>\n" f"📍 {item['address']}\n" f"💰 {item['price']}\n" f"📐 {item['price_per_m2']}\n" f"🔗 <a href='{item['url']}'>Смотреть на {site}</a>" ) if item.get("image_url"): try: await update.message.reply_photo( photo=item["image_url"], caption=message, parse_mode=TgParseMode.HTML ) except Exception as e: logger.warning(f"Не удалось отправить фото: {e}") await update.message.reply_text(message, parse_mode=TgParseMode.HTML) else: await update.message.reply_text(message, parse_mode=TgParseMode.HTML) await asyncio.sleep(0.5) if found >= 5: break if found == 0: await update.message.reply_text("📭 Ничего не найдено. Попробуйте позже.") else: await update.message.reply_text("✅ Загрузка завершена.", reply_markup=get_menu_keyboard()) except Exception as e: logger.error(f"Ошибка при получении объявлений: {e}") await update.message.reply_text( "❌ Ошибка при загрузке объявлений. Повторите попытку позже.", reply_markup=get_menu_keyboard() ) # --- Подписка --- async def handle_subscribe(update: Update, context: ContextTypes.DEFAULT_TYPE): user_id = update.effective_user.id ctx = user_context.get(user_id, {}) if not all([ctx.get("site"), ctx.get("city"), ctx.get("category")]): await update.message.reply_text( "❌ Сначала настройте парсинг через «🌐 Настройка парсинга»", reply_markup=get_menu_keyboard() ) return subscribe(user_id, ctx["site"], ctx["city"], ctx["category"]) await update.message.reply_text( f"✅ Подписка активирована!\n" f"🌐 {ctx['site']}\n" f"🏙 {ctx['city']}\n" f"🏠 {ctx['category']}\n\n" "Я буду присылать новые объявления.", reply_markup=get_menu_keyboard() ) # --- Отписка --- async def handle_unsubscribe(update: Update, context: ContextTypes.DEFAULT_TYPE): user_id = update.effective_user.id unsubscribe(user_id) await update.message.reply_text("❌ Вы отписаны от рассылки.", reply_markup=get_menu_keyboard()) # --- Главный обработчик --- async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): text = update.message.text user_id = update.effective_user.id logger.info(f"📩 Сообщение от {user_id}: {text}") if not text: return ctx = user_context.setdefault(user_id, {}) if ctx.get("awaiting_address"): await handle_info_request(update, context) return if text == "🏘 Инфо по адресу": await handle_info_request(update, context) return if text == "🌐 Настройка парсинга": await handle_parsing_setup(update, context) return if text in ["ЦИАН", "Avito", "Домофонд", "Яндекс.Недвижимость"]: await handle_parsing_setup(update, context) return if ctx.get("awaiting_city"): ctx["city"] = text ctx["awaiting_city"] = False ctx["awaiting_category"] = True await update.message.reply_text( "Выберите категорию:", reply_markup=get_categories_keyboard() ) return if ctx.get("awaiting_category"): if text in ["Квартиры", "Дома", "Коммерческая"]: ctx["category"] = text ctx["awaiting_category"] = False await update.message.reply_text( f"✅ Настройки сохранены!\n\n" f"🌐 Сайт: <b>{ctx['site']}</b>\n" f"🌆 Город: <b>{ctx['city']}</b>\n" f"🏠 Категория: <b>{ctx['category']}</b>", parse_mode=TgParseMode.HTML, reply_markup=get_menu_keyboard() ) else: await update.message.reply_text("❌ Выберите из меню.") return if text == "🔍 Посмотреть последние": await handle_last_listings(update, context) return elif text == "✅ Подписаться": await handle_subscribe(update, context) return elif text == "🚫 Отписаться": await handle_unsubscribe(update, context) return elif text == "📊 Статистика": await update.message.reply_text("📊 Пока недоступна.") return elif text == "❓ Помощь": await update.message.reply_text("ℹ️ Помощь по боту — в разработке.") return elif text == "🔙 Назад": await update.message.reply_text("Возврат в меню.", reply_markup=get_menu_keyboard()) return await update.message.reply_text("💡 Используйте меню ниже.", reply_markup=get_menu_keyboard()) # --- Запуск бота --- async def run_bot(): logger.info("🔄 run_bot() — старт функции") try: app = ApplicationBuilder().token(TELEGRAM_BOT_TOKEN).build() logger.info("✅ Telegram Application создана") # === Добавь job_queue ДО запуска === from bot.utils.subscriptions import check_new_listings app.job_queue.run_repeating(check_new_listings, interval=300, first=10) logger.info("✅ Планировщик задач (job_queue) запущен") app.add_handler(CommandHandler("start", start)) app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)) logger.info("🤖 Бот запущен и готов к работе") await app.initialize() await app.start() await app.updater.start_polling() await asyncio.Event().wait() except Exception as e: logger.error(f"❌ Ошибка: {e}")