diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2072a9b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +.git +.gitignore +venv/ +venv-google/ +logs/ +exports/ +data/ +database/*.db +database/*.db.bak +backups/ +nohup.out +scraper_gis/browser_context/ +scraper_gis/exports/ +**/__pycache__ +*.pyc +*.pyo +*.pyd +.export_progress +.phase2_progress +export.log +auditor_run.log +pipeline.log +web.log +web_debug.log +systemd.log diff --git a/handlers/ads.py b/handlers/ads.py index 129ac57..6452b8e 100644 --- a/handlers/ads.py +++ b/handlers/ads.py @@ -5,7 +5,7 @@ import logging from datetime import datetime, timedelta from aiogram import Router, F -from aiogram.types import Message, CallbackQuery, InputMediaPhoto +from aiogram.types import Message, CallbackQuery from aiogram.filters import Command, StateFilter from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.context import FSMContext @@ -15,7 +15,7 @@ from database.db import AsyncSessionLocal from database.models import Ad, User from utils.ui_styles import fmt_msg from aiogram.utils.keyboard import InlineKeyboardBuilder -from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.types import InlineKeyboardButton import config logger = logging.getLogger(__name__) @@ -37,17 +37,19 @@ def get_ads_main_menu(): builder.row(InlineKeyboardButton(text="🛒 Все товары", callback_data="ads_view_all")) builder.row(InlineKeyboardButton(text="➕ Создать лот", callback_data="ads_create")) builder.row(InlineKeyboardButton(text="👤 Мои лоты", callback_data="ads_my_items")) + builder.row(InlineKeyboardButton(text="🏠 В главное меню", callback_data="main_menu")) return builder.as_markup() def get_categories_kb(): builder = InlineKeyboardBuilder() cats = [ - ("Продам", "sell"), ("Куплю", "buy"), - ("Отдам", "give"), ("Услуги", "service") + ("Продам 💰", "sell"), ("Куплю 🛒", "buy"), + ("Отдам 🎁", "give"), ("Услуги 🔧", "service") ] for name, code in cats: builder.add(InlineKeyboardButton(text=name, callback_data=f"adcat_{code}")) builder.adjust(2) + builder.row(InlineKeyboardButton(text="🔙 В маркет", callback_data="ads_back")) return builder.as_markup() # ============================================================================= @@ -64,6 +66,19 @@ async def cmd_market(message: Message): ) await message.answer(text, reply_markup=get_ads_main_menu()) +@router.callback_query(F.data == "market") +@router.callback_query(F.data == "ads_back") +async def cb_market(callback: CallbackQuery, state: FSMContext): + """Главный вход в маркет из инлайн-кнопок""" + await state.clear() + text = ( + f"{fmt_msg('МАРКЕТ ЛКМ37', 'system')}\n\n" + "Здесь вы можете найти товары от соседей или предложить свои.\n" + "Все лоты проходят модерацию." + ) + from handlers.users import safe_edit_or_reply + await safe_edit_or_reply(callback, text, reply_markup=get_ads_main_menu()) + @router.callback_query(F.data == "ads_create") async def start_ad_creation(callback: CallbackQuery, state: FSMContext): await callback.message.answer("Выберите категорию для вашего лота:", reply_markup=get_categories_kb()) @@ -111,7 +126,7 @@ async def skip_photo(message: Message, state: FSMContext): await save_ad(message, data, None) await state.clear() -async def save_ad(message, data, photo_id): +async def save_ad(message: Message, data, photo_id): async with AsyncSessionLocal() as session: new_ad = Ad( user_id=message.from_user.id, @@ -120,25 +135,30 @@ async def save_ad(message, data, photo_id): description=data['description'], price=data['price'], photo_file_id=photo_id, - expires_at=datetime.utcnow() + timedelta(days=30) + expires_at=datetime.utcnow() + timedelta(days=30), + is_moderated=True ) session.add(new_ad) await session.commit() await message.answer( f"{fmt_msg('ЛОТ СОЗДАН', 'system')}\n\n" - "Ваше объявление отправлено на модерацию. После одобрения оно появится в общем списке." + "Ваше объявление успешно создано и опубликовано в маркете LKM37!" ) @router.callback_query(F.data == "ads_view_all") +async def view_ads_trigger(callback: CallbackQuery): + await view_ads(callback, page=0) + async def view_ads(callback: CallbackQuery, page: int = 0): async with AsyncSessionLocal() as session: - stmt = select(Ad).where(Ad.is_active == True, Ad.is_moderated == True).offset(page).limit(1) + stmt = select(Ad).where(Ad.is_active == True, Ad.is_moderated == True).order_by(Ad.created_at.desc()).offset(page).limit(1) res = await session.execute(stmt) ad = res.scalar_one_or_none() if not ad: - return await callback.message.answer("К сожалению, пока лотов нет.") + await callback.answer("К сожалению, пока лотов нет.", show_alert=True) + return price_text = f"{ad.price} ₽" if ad.price > 0 else "ДАРOM" text = ( @@ -155,11 +175,44 @@ async def view_ads(callback: CallbackQuery, page: int = 0): else: kb.row(InlineKeyboardButton(text="🤝 ЗАБРОНИРОВАНО", callback_data="ad_reserved_info")) - kb.row( - InlineKeyboardButton(text="⬅️", callback_data=f"ad_page_{page-1}"), - InlineKeyboardButton(text="➡️", callback_data=f"ad_page_{page+1}") - ) -... + # Пагинация + nav_buttons = [] + if page > 0: + nav_buttons.append(InlineKeyboardButton(text="⬅️ Предыдущий", callback_data=f"ad_page_{page-1}")) + + async with AsyncSessionLocal() as session: + next_stmt = select(Ad).where(Ad.is_active == True, Ad.is_moderated == True).offset(page + 1).limit(1) + next_res = await session.execute(next_stmt) + has_next = next_res.scalar_one_or_none() is not None + + if has_next: + nav_buttons.append(InlineKeyboardButton(text="Следующий ➡️", callback_data=f"ad_page_{page+1}")) + + if nav_buttons: + kb.row(*nav_buttons) + + kb.row(InlineKeyboardButton(text="🔙 В маркет", callback_data="ads_back")) + kb.row(InlineKeyboardButton(text="🏠 В главное меню", callback_data="main_menu")) + + try: + await callback.message.delete() + except Exception: + pass + + if ad.photo_file_id: + await callback.message.answer_photo(ad.photo_file_id, caption=text, reply_markup=kb.as_markup(), parse_mode='HTML') + else: + await callback.message.answer(text, reply_markup=kb.as_markup(), parse_mode='HTML') + await callback.answer() + +@router.callback_query(F.data.startswith("ad_page_")) +async def cb_ad_page(callback: CallbackQuery): + page = int(callback.data.split("_")[2]) + if page < 0: + await callback.answer("Это первое объявление.") + return + await view_ads(callback, page=page) + @router.callback_query(F.data.startswith("ad_reserve_")) async def reserve_ad(callback: CallbackQuery): ad_id = int(callback.data.split("_")[2]) @@ -178,17 +231,63 @@ async def reserve_ad(callback: CallbackQuery): f"Покупатель: ID{callback.from_user.id} (@{callback.from_user.username or '—'})", parse_mode='HTML' ) - except: pass + except Exception: + pass await callback.answer("✅ Товар забронирован. Свяжитесь с продавцом!", show_alert=True) - await callback.message.edit_reply_markup(reply_markup=None) # Обновить бы на "Забронировано" + # Обновим клавиатуру + kb = InlineKeyboardBuilder() + kb.row(InlineKeyboardButton(text="📧 Написать продавцу", url=f"tg://user?id={ad.user_id}")) + kb.row(InlineKeyboardButton(text="🤝 ЗАБРОНИРОВАНО", callback_data="ad_reserved_info")) + kb.row(InlineKeyboardButton(text="🔙 В маркет", callback_data="ads_back")) + try: + await callback.message.edit_reply_markup(reply_markup=kb.as_markup()) + except Exception: + pass else: - await callback.answer("❌ Товар уже забронирован или не найден.") - kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="ads_back")) + await callback.answer("❌ Товар уже забронирован или не найден.", show_alert=True) - if ad.photo_file_id: - await callback.message.answer_photo(ad.photo_file_id, caption=text, reply_markup=kb.as_markup()) - else: - await callback.message.answer(text, reply_markup=kb.as_markup()) +@router.callback_query(F.data == "ad_reserved_info") +async def cb_reserved_info(callback: CallbackQuery): + await callback.answer("Этот товар уже забронирован кем-то из соседей.", show_alert=True) + +@router.callback_query(F.data == "ads_my_items") +async def view_my_ads(callback: CallbackQuery): + user_id = callback.from_user.id + async with AsyncSessionLocal() as session: + stmt = select(Ad).where(Ad.user_id == user_id, Ad.is_active == True).order_by(Ad.created_at.desc()) + res = await session.execute(stmt) + ads = res.scalars().all() + + if not ads: + await callback.answer("У вас пока нет активных объявлений.", show_alert=True) + return + + text = "👤 ВАШИ ОБЪЯВЛЕНИЯ:\n\n" + kb = InlineKeyboardBuilder() + for ad in ads: + status = "🟢 Активно" if ad.is_moderated else "⏳ На модерации" + price_text = f"{ad.price} ₽" if ad.price > 0 else "Даром" + text += f"🔹 {ad.title} ({price_text})\n └ Статус: {status}\n\n" + kb.row( + InlineKeyboardButton(text=f"🗑️ Удалить '{ad.title[:15]}'", callback_data=f"ad_delete_{ad.ad_id}") + ) + kb.row(InlineKeyboardButton(text="🔙 В маркет", callback_data="ads_back")) + + from handlers.users import safe_edit_or_reply + await safe_edit_or_reply(callback, text, reply_markup=kb.as_markup()) await callback.answer() + +@router.callback_query(F.data.startswith("ad_delete_")) +async def delete_my_ad(callback: CallbackQuery): + ad_id = int(callback.data.split("_")[2]) + async with AsyncSessionLocal() as session: + ad = await session.get(Ad, ad_id) + if ad and ad.user_id == callback.from_user.id: + ad.is_active = False + await session.commit() + await callback.answer("✅ Объявление удалено.", show_alert=True) + else: + await callback.answer("❌ Объявление не найдено или вы не автор.", show_alert=True) + await view_my_ads(callback) diff --git a/handlers/users.py b/handlers/users.py index 97dd9fe..39e6014 100644 --- a/handlers/users.py +++ b/handlers/users.py @@ -8,6 +8,7 @@ from aiogram.types import Message, CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.filters import Command, CommandStart, StateFilter from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup from sqlalchemy import select, update from database.db import AsyncSessionLocal from database.models import User, InitiativeGroup @@ -286,10 +287,16 @@ async def process_location(message: Message): await message.answer("❌ Ошибка формата. Введите два числа через пробел.") +# Класс состояний обратной связи +class HelpFeedbackState(StatesGroup): + waiting_for_message = State() + waiting_for_reply = State() + @router.message(Command('help')) @router.callback_query(F.data == 'help') -async def cb_help(event): - text = ("❓ СПРАВКА\n\n" +async def cb_help(event, state: FSMContext): + await state.clear() + text = ("❓ СПРАВКА И ПОДДЕРЖКА\n\n" "• Используйте кнопки в главном меню для навигации.\n" "• Если бот не понимает команду, просто напишите вопрос текстом — наш ИИ постарается помочь.\n\n" "Список команд:\n" @@ -297,8 +304,130 @@ async def cb_help(event): "/phones — Телефоны\n" "/about — О доме\n" "/profile — Твой профиль\n" - "/rules — Правила чата") + "/rules — Правила чата\n\n" + "Вы можете отправить сообщение Инициативной группе / Администрации дома, нажав кнопку ниже.") kb = InlineKeyboardBuilder() + kb.row(InlineKeyboardButton(text="✍️ Написать администрации", callback_data="help_feedback")) kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="main_menu")) await safe_edit_or_reply(event, text, reply_markup=kb.as_markup(), parse_mode='HTML') + +@router.callback_query(F.data == "help_feedback") +async def start_help_feedback(callback: CallbackQuery, state: FSMContext): + await callback.answer() + await state.set_state(HelpFeedbackState.waiting_for_message) + + text = ("✍️ ОБРАТНАЯ СВЯЗЬ\n\n" + "Пожалуйста, введите текст вашего сообщения для администрации дома LKM37.\n" + "Вы также можете прикрепить одно фото или документ к вашему сообщению.\n\n" + "Для отмены нажмите кнопку ниже или введите /cancel.") + + kb = InlineKeyboardBuilder() + kb.row(InlineKeyboardButton(text="❌ Отмена", callback_data="main_menu")) + + await callback.message.answer(text, reply_markup=kb.as_markup(), parse_mode='HTML') + +@router.message(HelpFeedbackState.waiting_for_message) +async def process_help_feedback(message: Message, state: FSMContext): + if message.text and message.text.startswith('/'): + if message.text.lower() in ['/cancel', '/start']: + await state.clear() + await message.answer("❌ Отправка сообщения отменена.", reply_markup=get_main_menu()) + return + + user_id = message.from_user.id + + async with AsyncSessionLocal() as session: + user = await session.get(User, user_id) + + sender_name = message.from_user.full_name or f"user{user_id}" + username_str = f" (@{message.from_user.username})" if message.from_user.username else "" + apt_str = user.apartment or "Не указана" if user else "Не указана" + + admin_text = (f"📬 НОВОЕ ОБРАЩЕНИЕ ОТ ЖИЛЬЦА LKM37!\n\n" + f"👤 Отправитель: {sender_name}{username_str}\n" + f"🏢 Квартира: {apt_str}\n" + f"🆔 ID: {user_id}\n\n" + f"📝 Сообщение:\n{message.text or message.caption or '—'}") + + kb = InlineKeyboardBuilder() + kb.row(InlineKeyboardButton(text="✍️ Ответить жильцу", callback_data=f"help_reply_{user_id}")) + + try: + if message.photo: + await message.bot.send_photo( + chat_id=config.ADMIN_USER_ID, + photo=message.photo[-1].file_id, + caption=admin_text, + reply_markup=kb.as_markup(), + parse_mode='HTML' + ) + elif message.document: + await message.bot.send_document( + chat_id=config.ADMIN_USER_ID, + document=message.document.file_id, + caption=admin_text, + reply_markup=kb.as_markup(), + parse_mode='HTML' + ) + else: + await message.bot.send_message( + chat_id=config.ADMIN_USER_ID, + text=admin_text, + reply_markup=kb.as_markup(), + parse_mode='HTML' + ) + + await message.answer("✅ Ваше сообщение успешно отправлено!\n\nАдминистрация ответит вам в ближайшее время.", reply_markup=get_main_menu(), parse_mode='HTML') + except Exception as e: + logger.error(f"Ошибка при отправке фидбека админу: {e}") + await message.answer("❌ Произошла ошибка при отправке сообщения. Пожалуйста, попробуйте позже.", reply_markup=get_main_menu()) + + await state.clear() + +@router.callback_query(F.data.startswith("help_reply_")) +async def start_help_reply(callback: CallbackQuery, state: FSMContext): + if callback.from_user.id != config.ADMIN_USER_ID: + await callback.answer("🔒 Доступ только для суперадмина.", show_alert=True) + return + + target_user_id = int(callback.data.split("_")[2]) + await state.update_data(target_user_id=target_user_id) + await state.set_state(HelpFeedbackState.waiting_for_reply) + + await callback.answer() + await callback.message.answer(f"✍️ Введите ответ для пользователя {target_user_id}:\n\nДля отмены пришлите /cancel.", parse_mode='HTML') + +@router.message(HelpFeedbackState.waiting_for_reply) +async def process_help_reply(message: Message, state: FSMContext): + if message.from_user.id != config.ADMIN_USER_ID: + return + + if message.text and message.text.lower() in ['/cancel', 'отмена']: + await state.clear() + await message.answer("❌ Ответ отменен.", reply_markup=get_main_menu()) + return + + data = await state.get_data() + target_user_id = data.get("target_user_id") + + if not target_user_id: + await message.answer("❌ Ошибка: пользователь для ответа не найден в контексте.") + await state.clear() + return + + reply_text = (f"📩 ОТВЕТ АДМИНИСТРАЦИИ LKM37:\n\n" + f"{message.text}") + + try: + await message.bot.send_message( + chat_id=target_user_id, + text=reply_text, + parse_mode='HTML' + ) + await message.answer(f"✅ Ответ успешно отправлен пользователю {target_user_id}.", parse_mode='HTML') + except Exception as e: + logger.error(f"Ошибка при отправке ответа пользователю {target_user_id}: {e}") + await message.answer(f"❌ Не удалось отправить сообщение пользователю. Ошибка: {e}") + + await state.clear() diff --git a/keyboards/inline.py b/keyboards/inline.py index 25392e2..9ab1df5 100644 --- a/keyboards/inline.py +++ b/keyboards/inline.py @@ -26,7 +26,7 @@ def get_main_menu(verified: bool = False, is_ig: bool = False, is_admin: bool = ) builder.row( InlineKeyboardButton(text='📋 Опросы', callback_data='polls'), - InlineKeyboardButton(text=' События', callback_data='events') + InlineKeyboardButton(text='🎉 События', callback_data='events') ) builder.row( InlineKeyboardButton(text='🏪 Маркет', callback_data='market'), diff --git a/nohup.out b/nohup.out new file mode 100644 index 0000000..e69de29 diff --git a/scraper_gis/cabinet_links_map.json b/scraper_gis/cabinet_links_map.json new file mode 100644 index 0000000..d71ab24 --- /dev/null +++ b/scraper_gis/cabinet_links_map.json @@ -0,0 +1,382 @@ +[ + { + "text": "Справка по системе", + "href": "https://cdn.dom.gosuslugi.ru/webhelp" + }, + { + "text": "Регламенты и инструкции", + "href": "https://dom.gosuslugi.ru/#!/regulations" + }, + { + "text": "Все пользователи", + "href": "https://dom.gosuslugi.ru/#!/regulations?userCtgrCode=1" + }, + { + "text": "Граждане", + "href": "https://dom.gosuslugi.ru/#!/regulations?userCtgrCode=2" + }, + { + "text": "Кредитные организации и банковские агенты", + "href": "https://dom.gosuslugi.ru/#!/regulations?userCtgrCode=9" + }, + { + "text": "Органы государственного жилищного надзора", + "href": "https://dom.gosuslugi.ru/#!/regulations?userCtgrCode=8" + }, + { + "text": "Органы государственной власти", + "href": "https://dom.gosuslugi.ru/#!/regulations?userCtgrCode=3" + }, + { + "text": "Органы местного самоуправления", + "href": "https://dom.gosuslugi.ru/#!/regulations?userCtgrCode=4" + }, + { + "text": "Ответственные за реализацию приоритетного проекта \"Формирование комфортной городской среды\"", + "href": "https://dom.gosuslugi.ru/#!/regulations?userCtgrCode=14" + }, + { + "text": "Региональный оператор капитального ремонта", + "href": "https://dom.gosuslugi.ru/#!/regulations?userCtgrCode=10" + }, + { + "text": "Региональный оператор по обращению с твердыми коммунальными отходами", + "href": "https://dom.gosuslugi.ru/#!/regulations?userCtgrCode=12" + }, + { + "text": "Ресурсоснабжающие организации", + "href": "https://dom.gosuslugi.ru/#!/regulations?userCtgrCode=7" + }, + { + "text": "РКЦ, ЕИРЦ и платежные агенты", + "href": "https://dom.gosuslugi.ru/#!/regulations?userCtgrCode=13" + }, + { + "text": "Управляющие организации, ТСЖ, ЖК, ЖСК", + "href": "https://dom.gosuslugi.ru/#!/regulations?userCtgrCode=6" + }, + { + "text": "Федеральные органы власти", + "href": "https://dom.gosuslugi.ru/#!/regulations?userCtgrCode=5" + }, + { + "text": "Фонд содействия реформированию ЖКХ", + "href": "https://dom.gosuslugi.ru/#!/regulations?userCtgrCode=11" + }, + { + "text": "Часто задаваемые вопросы", + "href": "https://dom.gosuslugi.ru/#!/faq" + }, + { + "text": "Карта сайта", + "href": "https://dom.gosuslugi.ru/#!/site-map" + }, + { + "text": "Версия для слабовидящих", + "href": "https://dom.gosuslugi.ru/special/" + }, + { + "text": "Перейти на новый сайт", + "href": "https://portal.dom.gosuslugi.ru/" + }, + { + "text": "ГИС ЖКХ", + "href": "https://dom.gosuslugi.ru/#!/" + }, + { + "text": "Обращение в службу поддержки", + "href": "https://dom.gosuslugi.ru/#!/support-cabinet" + }, + { + "text": "Зарегистрироваться", + "href": "https://dom.gosuslugi.ru/#!/registration-info" + }, + { + "text": "Получатели услуг", + "href": "https://dom.gosuslugi.ru/#!/audience/consumers" + }, + { + "text": "Поставщики услуг", + "href": "https://dom.gosuslugi.ru/#!/audience/suppliers" + }, + { + "text": "Органы власти", + "href": "https://dom.gosuslugi.ru/#!/audience/authorities" + }, + { + "text": "Учебники", + "href": "https://dom.gosuslugi.ru/#!/regulations?rubricNames=%D0%A3%D1%87%D0%B5%D0%B1%D0%BD%D0%B8%D0%BA%D0%B8" + }, + { + "text": "Видео ГИС ЖКХ", + "href": "https://dom.gosuslugi.ru/#!/videos/search?categories=1" + }, + { + "text": "Реестр программ в сфере ЖКХ", + "href": "https://dom.gosuslugi.ru/#!/programs/list" + }, + { + "text": "Реестр программ капитального ремонта", + "href": "https://dom.gosuslugi.ru/#!/capital-repair/programs" + }, + { + "text": "Федеральный проект «Формирование комфортной городской среды»", + "href": "https://sreda.dom.gosuslugi.ru/" + }, + { + "text": "Реестр объектов жилищного фонда", + "href": "https://dom.gosuslugi.ru/#!/houses" + }, + { + "text": "Реестры поставщиков информации", + "href": "https://dom.gosuslugi.ru/" + }, + { + "text": "Органы государственной власти и местного самоуправления", + "href": "https://dom.gosuslugi.ru/#!/organizations?orgType=7&orgType=12&orgType=4&orgType=6&orgType=17&orgType=5&orgType=8&orgType=10&orgType=16&orgType=28&orgType=29&doSearch=false&restore=false" + }, + { + "text": "Региональные операторы капитального ремонта", + "href": "https://dom.gosuslugi.ru/#!/organizations?orgType=14&doSearch=false&restore=false" + }, + { + "text": "Организации, осуществляющие управление многоквартирными\n домами", + "href": "https://dom.gosuslugi.ru/#!/organizations?orgType=1&orgType=19&orgType=22&orgType=21&orgType=20&doSearch=false&restore=false" + }, + { + "text": "Ресурсоснабжающие организации", + "href": "https://dom.gosuslugi.ru/#!/organizations?orgType=2&doSearch=false&restore=false" + }, + { + "text": "Региональные операторы по обращению с твердыми коммунальными\n отходами", + "href": "https://dom.gosuslugi.ru/#!/organizations?orgType=18&doSearch=false&restore=false" + }, + { + "text": "Платежные агенты", + "href": "https://dom.gosuslugi.ru/#!/organizations?orgType=24&orgType=25&doSearch=false&restore=false" + }, + { + "text": "Банки, иные кредитные организации или органы, осуществляющие\n открытиеи ведение лицевых счетов в соответствии с\n бюджетным законодательством РФ", + "href": "https://dom.gosuslugi.ru/#!/organizations?orgType=26&doSearch=false&restore=false" + }, + { + "text": "Иные поставщики информации", + "href": "https://dom.gosuslugi.ru/#!/organizations?orgType=23&orgType=3&orgType=15&orgType=11&orgType=13&orgType=30&doSearch=false&restore=false" + }, + { + "text": "Реестр контрольных (надзорных) мероприятий", + "href": "https://dom.gosuslugi.ru/#!/rp" + }, + { + "text": "Реестр информации о готовности к отопительному сезону (периоду)", + "href": "https://dom.gosuslugi.ru/#!/rpg/readiness-passports-search-public" + }, + { + "text": "Информация о местах накопления", + "href": "https://dom.gosuslugi.ru/#!/public-tko/tko-search" + }, + { + "text": "Информация о конкурсах по отбору региональных операторов", + "href": "https://dom.gosuslugi.ru/#!/tkoregop" + }, + { + "text": "Сводный федеральный реестр лицензий", + "href": "https://dom.gosuslugi.ru/#!/rls-public/licenses" + }, + { + "text": "Реестр дисквалифицированных лиц", + "href": "https://dom.gosuslugi.ru/#!/licenses/person/search" + }, + { + "text": "Реестр административных правонарушений", + "href": "https://dom.gosuslugi.ru/#!/public/rap-rp" + }, + { + "text": "Перечень органов власти", + "href": "https://dom.gosuslugi.ru/#!/authorities" + }, + { + "text": "Реестр решений об изъятии земельных участков", + "href": "https://dom.gosuslugi.ru/#!/withdrawal/search" + }, + { + "text": "Перечень населенных пунктов без доступа к сети Интернет", + "href": "https://dom.gosuslugi.ru/#!/places/without/internet" + }, + { + "text": "Реестр управляющих организаций и решений об определении управляющих\n организаций", + "href": "https://dom.gosuslugi.ru/#!/tr-mo-pub/registry/organizations" + }, + { + "text": "Реестр информации о конкурсах по отбору кредитных организаций", + "href": "https://dom.gosuslugi.ru/#!/capital-repair/tenders" + }, + { + "text": "Оснащенность индивидуальными приборами учета", + "href": "https://dom.gosuslugi.ru/#!/individual-meters" + }, + { + "text": "Оснащенность общедомовыми приборами учета", + "href": "https://dom.gosuslugi.ru/#!/common-meters" + }, + { + "text": "Техническое состояние многоквартирных домов", + "href": "https://dom.gosuslugi.ru/#!/houses-condition" + }, + { + "text": "Способы управления многоквартирными домами", + "href": "https://dom.gosuslugi.ru/#!/wdgt-mkd-control-method" + }, + { + "text": "Статистика по управлению МКД временными управляющими организациями", + "href": "https://dom.gosuslugi.ru/#!/transitory-mo" + }, + { + "text": "Работа с обращениями граждан", + "href": "https://dom.gosuslugi.ru/#!/pafo/appeals" + }, + { + "text": "Проверки, проведенные контролирующими органами", + "href": "https://dom.gosuslugi.ru/#!/examinations" + }, + { + "text": "Привлечение ресурсоснабжающих организаций к административной ответственности", + "href": "https://dom.gosuslugi.ru/#!/resource-liability" + }, + { + "text": "Привлечение управляющих организаций к административной ответственности", + "href": "https://dom.gosuslugi.ru/#!/managing-liability" + }, + { + "text": "Способ формирования фонда капитального ремонта", + "href": "https://dom.gosuslugi.ru/#!/crp-fund-forming" + }, + { + "text": "Реализация краткосрочных планов капитального ремонта", + "href": "https://dom.gosuslugi.ru/#!/crp-plans-execution" + }, + { + "text": "Размещение информации в субъектах РФ", + "href": "https://dom.gosuslugi.ru/#!/implementation-map" + }, + { + "text": "Тарифы на оплату коммунальных ресурсов", + "href": "https://dom.gosuslugi.ru/#!/public-tariff/communal-resources/search" + }, + { + "text": "Размер платы за содержание жилого помещения", + "href": "https://dom.gosuslugi.ru/#!/public-tariff/extra/CAPITAL_REPAIR/search" + }, + { + "text": "Минимальный размер взноса на капитальный ремонт", + "href": "https://dom.gosuslugi.ru/#!/public-tariff/extra/CAPITAL_REPAIR_MIN_CONTRIBUTION/search" + }, + { + "text": "Социальная норма потребления электрической энергии\n (мощности)", + "href": "https://dom.gosuslugi.ru/#!/public-tariff/extra/ELECTRICITY_CONSUMPTION_SOCIAL_NORM/search" + }, + { + "text": "Размер платы за пользование жилым помещением", + "href": "https://dom.gosuslugi.ru/#!/public-tariff/extra/RENT_PAYMENT/search" + }, + { + "text": "Нормативные правовые акты в сфере регулирования тарифов", + "href": "https://dom.gosuslugi.ru/#!/rates" + }, + { + "text": "Льготные тарифы на жилищно-коммунальные услуги", + "href": "https://dom.gosuslugi.ru/#!/public-tariff/extra/reduced-search" + }, + { + "text": "Индексы изменения платы граждан за ЖКУ", + "href": "https://dom.gosuslugi.ru/#!/public-indices" + }, + { + "text": "Компенсации расходов отдельным категориям граждан", + "href": "https://dom.gosuslugi.ru/#!/msp/public/categories/" + }, + { + "text": "Субсидии на оплату ЖКУ", + "href": "https://dom.gosuslugi.ru/#!/msp/public/subsidy-provisions/" + }, + { + "text": "Нормативные правовые акты в сфере мер социальной поддержки", + "href": "https://dom.gosuslugi.ru/#!/subsidies" + }, + { + "text": "Нормативы потребления коммунальных услуг", + "href": "https://dom.gosuslugi.ru/#!/public-rate-consumption/list" + }, + { + "text": "Информация о стандартах", + "href": "https://dom.gosuslugi.ru/#!/public-standart" + }, + { + "text": "Мероприятия общественного жилищного контроля", + "href": "https://dom.gosuslugi.ru/#!/ojk/search" + }, + { + "text": "Информация о плановых перерывах в предоставлении КУ", + "href": "https://dom.gosuslugi.ru/#!/planned-outage/list" + }, + { + "text": "Открытые данные", + "href": "https://dom.gosuslugi.ru/#!/open-data" + }, + { + "text": "Новости и события", + "href": "https://dom.gosuslugi.ru/#!/news/search" + }, + { + "text": "Обращаем внимание пользователей ГИС ЖКХ и \"Госуслуги Дом\"!", + "href": "https://dom.gosuslugi.ru/#!/newsView?newsId=ff437377-2bd9-44c7-831d-53470bf154ff&audience=MAIN_PAGE&fromPlace=main" + }, + { + "text": "Уведомление пользователей системы", + "href": "https://dom.gosuslugi.ru/#!/newsView?newsId=f8cba8bd-2025-4246-b433-fe0b8b38ecd1&audience=MAIN_PAGE&fromPlace=main" + }, + { + "text": "Перечень организаций жилищно-коммунального хозяйства, участвующих в эксперименте по взысканию задолженности.", + "href": "https://dom.gosuslugi.ru/#!/newsView?newsId=e4a3ee99-0e98-4022-9757-41d440b0e1bb&audience=MAIN_PAGE&fromPlace=main" + }, + { + "text": "Пояснение к критериям отбора участников в рамках эксперимента по взысканию задолженности", + "href": "https://dom.gosuslugi.ru/#!/newsView?newsId=8c9898f4-fe19-4f13-a139-214e1973a9a7&audience=MAIN_PAGE&fromPlace=main" + }, + { + "text": "Все сервисы", + "href": "https://dom.gosuslugi.ru/#!/eServices" + }, + { + "text": "ЖИЛИЩНЫЙ КОДЕКС РФ", + "href": "https://dom.gosuslugi.ru/filestore/publicDownloadServlet?context=contentmanagement&uid=c4f8bc28-c4bb-4253-925c-b85389dc22ca" + }, + { + "text": "Федеральный закон от 21.07.2014 № 209-ФЗ \"О государственной информационной системе жилищно-коммунального хозяйства\"", + "href": "https://dom.gosuslugi.ru/filestore/publicDownloadServlet?context=contentmanagement&uid=6e33668f-69e1-43c1-972e-b8553c789b4d" + }, + { + "text": "Законодательство по ГИС ЖКХ", + "href": "https://dom.gosuslugi.ru/#!/legislation" + }, + { + "text": "Законодательство РФ", + "href": "http://dom.gosuslugi.ru/#!/legislationRF" + }, + { + "text": "Все мероприятия", + "href": "https://dom.gosuslugi.ru/#!/events-calendar/search" + }, + { + "text": "Обу­ча­ющие ма­тери­алы", + "href": "https://my.dom.gosuslugi.ru/#!/regulations" + }, + { + "text": "Все ссылки", + "href": "https://dom.gosuslugi.ru/#!/useful-links" + }, + { + "text": "Карта внедрения ГИС ЖКХ в субъектах Российской Федерации", + "href": "https://dom.gosuslugi.ru/#!/implementation-map" + } +] \ No newline at end of file diff --git a/scraper_gis/docker-compose.yml b/scraper_gis/docker-compose.yml index 596677b..9ba0a50 100644 --- a/scraper_gis/docker-compose.yml +++ b/scraper_gis/docker-compose.yml @@ -16,4 +16,4 @@ services: - TZ=Europe/Ulyanovsk - TELEGRAM_BOT_TOKEN=8725618164:AAH1tGalq-pw1l0t4P5c0sdLkCVJdh7IE7M - TELEGRAM_CHAT_ID=197957361 - command: ["./start.sh", "ultimate"] + command: ["./start.sh", "interactive"] diff --git a/scraper_gis/exports/01_Main_Dashboard.png b/scraper_gis/exports/01_Main_Dashboard.png new file mode 100644 index 0000000..a8220e4 Binary files /dev/null and b/scraper_gis/exports/01_Main_Dashboard.png differ diff --git a/scraper_gis/exports/02_Appeals_view.png b/scraper_gis/exports/02_Appeals_view.png new file mode 100644 index 0000000..bf4a587 Binary files /dev/null and b/scraper_gis/exports/02_Appeals_view.png differ diff --git a/scraper_gis/exports/02_Bills_view.png b/scraper_gis/exports/02_Bills_view.png new file mode 100644 index 0000000..f124afd Binary files /dev/null and b/scraper_gis/exports/02_Bills_view.png differ diff --git a/scraper_gis/exports/02_Main_view.png b/scraper_gis/exports/02_Main_view.png new file mode 100644 index 0000000..32ec99f Binary files /dev/null and b/scraper_gis/exports/02_Main_view.png differ diff --git a/scraper_gis/exports/02_Voting_view.png b/scraper_gis/exports/02_Voting_view.png new file mode 100644 index 0000000..58322cf Binary files /dev/null and b/scraper_gis/exports/02_Voting_view.png differ diff --git a/scraper_gis/mcp_full_parser.py b/scraper_gis/mcp_full_parser.py new file mode 100644 index 0000000..ee9bd8d --- /dev/null +++ b/scraper_gis/mcp_full_parser.py @@ -0,0 +1,87 @@ +from playwright.sync_api import sync_playwright +import time +import os + +EXPORT_DIR = "/app/exports" +if not os.path.exists(EXPORT_DIR): + os.makedirs(EXPORT_DIR) + +print("🚀 Запускаем глубокий скрейпинг Личного кабинета ГИС ЖКХ...") + +def handle_download(download): + print(f"📥 Обнаружено скачивание файла: {download.suggested_filename}") + filepath = os.path.join(EXPORT_DIR, download.suggested_filename) + download.save_as(filepath) + print(f"✅ Файл успешно сохранен: {filepath}") + +with sync_playwright() as p: + browser = p.chromium.launch_persistent_context( + user_data_dir="/app/browser_context", + headless=True, + accept_downloads=True, + args=[ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu', + '--window-size=1920,1080' + ] + ) + + page = browser.pages[0] + # Привязываем обработчик скачиваний + page.on("download", handle_download) + + # 1. Переход на главную + print("🌐 Переходим на главную страницу ЛК (dom.gosuslugi.ru)...") + page.goto("https://dom.gosuslugi.ru/#!/main") + page.wait_for_timeout(7000) # Ждем прогрузки дашборда + + # Скриншот главной + page.screenshot(path=os.path.join(EXPORT_DIR, "01_Main_Dashboard.png"), full_page=True) + print("📸 Скриншот: Главный дашборд сохранен.") + + # 2. Ищем и кликаем по основным разделам + sections = [ + {"name": "Обращения", "text": "Обращения"}, + {"name": "Оплата_ЖКУ", "text": "Оплата"}, + {"name": "Договоры", "text": "Договор"}, + {"name": "Голосования", "text": "Голосования"} + ] + + for i, sec in enumerate(sections, start=2): + print(f"🔍 Ищем раздел: {sec['name']}...") + try: + # Ищем ссылку или кнопку, содержащую нужный текст (игнорируем регистр) + # В SPA-приложениях вроде ГИС ЖКХ элементы часто подгружаются динамически + element = page.locator(f"text=/{sec['text']}/i").first + + if element.is_visible(): + print(f"✅ Найден раздел {sec['name']}, кликаем...") + element.click() + page.wait_for_timeout(5000) # Ждем прогрузки страницы + + # Скриншот раздела + scr_name = f"{i:02d}_{sec['name']}.png" + page.screenshot(path=os.path.join(EXPORT_DIR, scr_name), full_page=True) + print(f"📸 Скриншот раздела сохранен: {scr_name}") + + # Поиск кнопок "Скачать" или "Экспорт" + print("⏬ Проверяем наличие вложений для скачивания...") + download_btns = page.locator("text=/скачать|экспорт|download|pdf|docx|zip/i").all() + for btn in download_btns: + if btn.is_visible(): + print("Кликаем по кнопке скачивания...") + btn.click() + page.wait_for_timeout(3000) # Даем время на инициализацию загрузки + + # Возврат на главную для следующего цикла + page.goto("https://dom.gosuslugi.ru/#!/main") + page.wait_for_timeout(4000) + else: + print(f"⚠️ Раздел {sec['name']} не найден или скрыт.") + except Exception as e: + print(f"❌ Ошибка при переходе в {sec['name']}: {e}") + + browser.close() + print("🏁 Скрейпинг завершен! Все файлы в папке /app/exports") diff --git a/scraper_gis/mcp_smart_parser.py b/scraper_gis/mcp_smart_parser.py new file mode 100644 index 0000000..66e6ef4 --- /dev/null +++ b/scraper_gis/mcp_smart_parser.py @@ -0,0 +1,88 @@ +from playwright.sync_api import sync_playwright +import time +import os +import json + +EXPORT_DIR = "/app/exports" +API_LOG_FILE = os.path.join(EXPORT_DIR, "api_responses.log") + +if not os.path.exists(EXPORT_DIR): + os.makedirs(EXPORT_DIR) + +# Очищаем лог перед новым запуском +with open(API_LOG_FILE, "w", encoding="utf-8") as f: + f.write("") + +print("🚀 Запускаем Умного Робота-Перехватчика ГИС ЖКХ...") + +def handle_response(response): + # Ловим только JSON ответы, чтобы найти внутренние API Госуслуг + if "application/json" in response.headers.get("content-type", ""): + try: + url = response.url + # Игнорируем метрики и мусорные запросы + if "metric" in url or "log" in url or "stat" in url: + return + + body = response.json() + + # Сохраняем интересные ответы в лог для анализа + # Нас интересуют ответы, где есть списки элементов (массивы) или файлы + if isinstance(body, dict) or isinstance(body, list): + # Просто чтобы не писать гигантские дампы, пишем URL и начало ответа + with open(API_LOG_FILE, "a", encoding="utf-8") as f: + f.write(f"\\n--- URL: {url} ---\\n") + json.dump(body, f, ensure_ascii=False, indent=2) + f.write("\\n") + + except Exception as e: + pass # Игнорируем ошибки парсинга (иногда JSON бывает битым или пустым) + +with sync_playwright() as p: + browser = p.chromium.launch_persistent_context( + user_data_dir="/app/browser_context", + headless=True, + args=[ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu', + '--window-size=1920,1080' + ] + ) + + page = browser.pages[0] + + # Включаем прослушку всех ответов от сервера! + page.on("response", handle_response) + + print("🌐 Подключаемся к Личному кабинету...") + + # Задаем список прямых ссылок (роутов) для обхода + routes = [ + {"name": "Main", "url": "https://dom.gosuslugi.ru/#!/main"}, + {"name": "Appeals", "url": "https://dom.gosuslugi.ru/#!/appeals"}, # Обращения + {"name": "Bills", "url": "https://dom.gosuslugi.ru/#!/bills"}, # Счета + {"name": "Voting", "url": "https://dom.gosuslugi.ru/#!/voting"} # Голосования + ] + + for route in routes: + print(f"\\n➡️ Переход в раздел: {route['name']}") + try: + page.goto(route['url']) + # Ждем 10 секунд, чтобы API успело отдать все JSON с документами + print("⏳ Ожидание загрузки данных через API (10 сек)...") + page.wait_for_timeout(10000) + + scr_name = f"02_{route['name']}_view.png" + page.screenshot(path=os.path.join(EXPORT_DIR, scr_name), full_page=True) + print(f"📸 Скриншот раздела сохранен: {scr_name}") + + except Exception as e: + print(f"❌ Ошибка в разделе {route['name']}: {e}") + + browser.close() + + print("\\n🏁 Сбор данных завершен!") + print(f"📂 Лог API запросов сохранен в: {API_LOG_FILE}") + print("🕵️‍♂️ Теперь мы можем проанализировать этот лог и вытащить оттуда прямые ссылки на скачивание PDF-файлов!") diff --git a/scraper_gis/mcp_test_auth.py b/scraper_gis/mcp_test_auth.py new file mode 100644 index 0000000..f5c499c --- /dev/null +++ b/scraper_gis/mcp_test_auth.py @@ -0,0 +1,28 @@ +from playwright.sync_api import sync_playwright +import time +import os + +print("🚀 Запускаем тестовый браузер для авторизации...") + +with sync_playwright() as p: + browser = p.chromium.launch_persistent_context( + user_data_dir="/app/browser_context", + headless=False, + args=[ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu', + '--window-size=1280,1024' + ] + ) + page = browser.pages[0] + + print("🌐 Переходим на dom.gosuslugi.ru...") + page.goto("https://dom.gosuslugi.ru/") + + print("⏳ Окно открыто! У тебя есть 5 минут, чтобы авторизоваться через noVNC.") + time.sleep(300) + + browser.close() + print("✅ Сессия сохранена, браузер закрыт.") diff --git a/scraper_gis/mcp_test_cabinet.py b/scraper_gis/mcp_test_cabinet.py new file mode 100644 index 0000000..0b354b3 --- /dev/null +++ b/scraper_gis/mcp_test_cabinet.py @@ -0,0 +1,54 @@ +from playwright.sync_api import sync_playwright +import time +import json + +print("🚀 Запускаем Headless-агента для создания карты личного кабинета...") + +with sync_playwright() as p: + # Запускаем уже в фоновом (headless) режиме, так как куки сохранены! + browser = p.chromium.launch_persistent_context( + user_data_dir="/app/browser_context", + headless=True, + args=[ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu', + '--window-size=1280,1024' + ] + ) + + page = browser.pages[0] + + print("🌐 Переходим на главную страницу ЛК (dom.gosuslugi.ru)...") + page.goto("https://dom.gosuslugi.ru/#!/main") + + # Ждем 5 секунд, чтобы все скрипты и данные кабинета прогрузились + page.wait_for_timeout(5000) + + print("📸 Делаем полноразмерный скриншот кабинета...") + page.screenshot(path="/app/cabinet_home_map.png", full_page=True) + + print("🗺️ Составляем карту ссылок (парсим меню)...") + # Выполняем JS прямо на странице, чтобы вытащить структуру + links = page.evaluate('''() => { + let menu_links = []; + // Собираем все осмысленные ссылки + document.querySelectorAll('a').forEach(a => { + let text = a.innerText.trim(); + if (a.href && text.length > 0 && a.href.includes('dom.gosuslugi.ru')) { + menu_links.push({text: text, href: a.href}); + } + }); + return menu_links; + }''') + + # Сохраняем карту ссылок в JSON + with open("/app/cabinet_links_map.json", "w", encoding="utf-8") as f: + json.dump(links, f, ensure_ascii=False, indent=2) + + print(f"✅ Готово! Найдено уникальных ссылок: {len(links)}") + print("💾 Скриншот: cabinet_home_map.png") + print("💾 Карта: cabinet_links_map.json") + + browser.close()