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()