feat: добавлена обратная связь с FSM в раздел Помощь, эмодзи в меню, исправлен маркет в боте

This commit is contained in:
Admin 2026-06-26 21:29:52 +04:00
parent f9116f14e9
commit a1ae78e5f8
16 changed files with 919 additions and 27 deletions

25
.dockerignore Normal file
View file

@ -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

View file

@ -5,7 +5,7 @@
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from aiogram import Router, F 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.filters import Command, StateFilter
from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
@ -15,7 +15,7 @@ from database.db import AsyncSessionLocal
from database.models import Ad, User from database.models import Ad, User
from utils.ui_styles import fmt_msg from utils.ui_styles import fmt_msg
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton from aiogram.types import InlineKeyboardButton
import config import config
logger = logging.getLogger(__name__) 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_view_all"))
builder.row(InlineKeyboardButton(text=" Создать лот", callback_data="ads_create")) builder.row(InlineKeyboardButton(text=" Создать лот", callback_data="ads_create"))
builder.row(InlineKeyboardButton(text="👤 Мои лоты", callback_data="ads_my_items")) builder.row(InlineKeyboardButton(text="👤 Мои лоты", callback_data="ads_my_items"))
builder.row(InlineKeyboardButton(text="🏠 В главное меню", callback_data="main_menu"))
return builder.as_markup() return builder.as_markup()
def get_categories_kb(): def get_categories_kb():
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
cats = [ cats = [
("Продам", "sell"), ("Куплю", "buy"), ("Продам 💰", "sell"), ("Куплю 🛒", "buy"),
("Отдам", "give"), ("Услуги", "service") ("Отдам 🎁", "give"), ("Услуги 🔧", "service")
] ]
for name, code in cats: for name, code in cats:
builder.add(InlineKeyboardButton(text=name, callback_data=f"adcat_{code}")) builder.add(InlineKeyboardButton(text=name, callback_data=f"adcat_{code}"))
builder.adjust(2) builder.adjust(2)
builder.row(InlineKeyboardButton(text="🔙 В маркет", callback_data="ads_back"))
return builder.as_markup() return builder.as_markup()
# ============================================================================= # =============================================================================
@ -64,6 +66,19 @@ async def cmd_market(message: Message):
) )
await message.answer(text, reply_markup=get_ads_main_menu()) 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"
"<i>Все лоты проходят модерацию.</i>"
)
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") @router.callback_query(F.data == "ads_create")
async def start_ad_creation(callback: CallbackQuery, state: FSMContext): async def start_ad_creation(callback: CallbackQuery, state: FSMContext):
await callback.message.answer("Выберите категорию для вашего лота:", reply_markup=get_categories_kb()) 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 save_ad(message, data, None)
await state.clear() 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: async with AsyncSessionLocal() as session:
new_ad = Ad( new_ad = Ad(
user_id=message.from_user.id, user_id=message.from_user.id,
@ -120,25 +135,30 @@ async def save_ad(message, data, photo_id):
description=data['description'], description=data['description'],
price=data['price'], price=data['price'],
photo_file_id=photo_id, 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) session.add(new_ad)
await session.commit() await session.commit()
await message.answer( await message.answer(
f"{fmt_msg('ЛОТ СОЗДАН', 'system')}\n\n" f"{fmt_msg('ЛОТ СОЗДАН', 'system')}\n\n"
"Ваше объявление отправлено на модерацию. После одобрения оно появится в общем списке." "Ваше объявление успешно создано и опубликовано в маркете LKM37!"
) )
@router.callback_query(F.data == "ads_view_all") @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 def view_ads(callback: CallbackQuery, page: int = 0):
async with AsyncSessionLocal() as session: 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) res = await session.execute(stmt)
ad = res.scalar_one_or_none() ad = res.scalar_one_or_none()
if not ad: 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" price_text = f"{ad.price}" if ad.price > 0 else "ДАРOM"
text = ( text = (
@ -155,11 +175,44 @@ async def view_ads(callback: CallbackQuery, page: int = 0):
else: else:
kb.row(InlineKeyboardButton(text="🤝 ЗАБРОНИРОВАНО", callback_data="ad_reserved_info")) kb.row(InlineKeyboardButton(text="🤝 ЗАБРОНИРОВАНО", callback_data="ad_reserved_info"))
kb.row( # Пагинация
InlineKeyboardButton(text="⬅️", callback_data=f"ad_page_{page-1}"), nav_buttons = []
InlineKeyboardButton(text="➡️", callback_data=f"ad_page_{page+1}") 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_")) @router.callback_query(F.data.startswith("ad_reserve_"))
async def reserve_ad(callback: CallbackQuery): async def reserve_ad(callback: CallbackQuery):
ad_id = int(callback.data.split("_")[2]) 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 ''})", f"Покупатель: ID{callback.from_user.id} (@{callback.from_user.username or ''})",
parse_mode='HTML' parse_mode='HTML'
) )
except: pass except Exception:
pass
await callback.answer("✅ Товар забронирован. Свяжитесь с продавцом!", show_alert=True) 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: else:
await callback.answer("❌ Товар уже забронирован или не найден.") await callback.answer("❌ Товар уже забронирован или не найден.", show_alert=True)
kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="ads_back"))
if ad.photo_file_id: @router.callback_query(F.data == "ad_reserved_info")
await callback.message.answer_photo(ad.photo_file_id, caption=text, reply_markup=kb.as_markup()) async def cb_reserved_info(callback: CallbackQuery):
else: await callback.answer("Этот товар уже забронирован кем-то из соседей.", show_alert=True)
await callback.message.answer(text, reply_markup=kb.as_markup())
@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 = "<b>👤 ВАШИ ОБЪЯВЛЕНИЯ:</b>\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"🔹 <b>{ad.title}</b> ({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() 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)

View file

@ -8,6 +8,7 @@ from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.filters import Command, CommandStart, StateFilter from aiogram.filters import Command, CommandStart, StateFilter
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from sqlalchemy import select, update from sqlalchemy import select, update
from database.db import AsyncSessionLocal from database.db import AsyncSessionLocal
from database.models import User, InitiativeGroup from database.models import User, InitiativeGroup
@ -286,10 +287,16 @@ async def process_location(message: Message):
await message.answer("❌ Ошибка формата. Введите два числа через пробел.") await message.answer("❌ Ошибка формата. Введите два числа через пробел.")
# Класс состояний обратной связи
class HelpFeedbackState(StatesGroup):
waiting_for_message = State()
waiting_for_reply = State()
@router.message(Command('help')) @router.message(Command('help'))
@router.callback_query(F.data == 'help') @router.callback_query(F.data == 'help')
async def cb_help(event): async def cb_help(event, state: FSMContext):
text = ("<b>❓ СПРАВКА</b>\n\n" await state.clear()
text = ("<b>❓ СПРАВКА И ПОДДЕРЖКА</b>\n\n"
"• Используйте кнопки в главном меню для навигации.\n" "• Используйте кнопки в главном меню для навигации.\n"
"• Если бот не понимает команду, просто напишите вопрос текстом — наш ИИ постарается помочь.\n\n" "• Если бот не понимает команду, просто напишите вопрос текстом — наш ИИ постарается помочь.\n\n"
"<b>Список команд:</b>\n" "<b>Список команд:</b>\n"
@ -297,8 +304,130 @@ async def cb_help(event):
"/phones — Телефоны\n" "/phones — Телефоны\n"
"/about — О доме\n" "/about — О доме\n"
"/profile — Твой профиль\n" "/profile — Твой профиль\n"
"/rules — Правила чата") "/rules — Правила чата\n\n"
"<i>Вы можете отправить сообщение Инициативной группе / Администрации дома, нажав кнопку ниже.</i>")
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
kb.row(InlineKeyboardButton(text="✍️ Написать администрации", callback_data="help_feedback"))
kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="main_menu")) kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="main_menu"))
await safe_edit_or_reply(event, text, reply_markup=kb.as_markup(), parse_mode='HTML') 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 = ("✍️ <b>ОБРАТНАЯ СВЯЗЬ</b>\n\n"
"Пожалуйста, введите текст вашего сообщения для администрации дома LKM37.\n"
"Вы также можете прикрепить одно фото или документ к вашему сообщению.\n\n"
"<i>Для отмены нажмите кнопку ниже или введите /cancel.</i>")
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"📬 <b>НОВОЕ ОБРАЩЕНИЕ ОТ ЖИЛЬЦА LKM37!</b>\n\n"
f"👤 Отправитель: <b>{sender_name}</b>{username_str}\n"
f"🏢 Квартира: <b>{apt_str}</b>\n"
f"🆔 ID: <code>{user_id}</code>\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("✅ <b>Ваше сообщение успешно отправлено!</b>\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"✍️ Введите ответ для пользователя <code>{target_user_id}</code>:\n\n<i>Для отмены пришлите /cancel.</i>", 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"📩 <b>ОТВЕТ АДМИНИСТРАЦИИ LKM37:</b>\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"✅ Ответ успешно отправлен пользователю <code>{target_user_id}</code>.", parse_mode='HTML')
except Exception as e:
logger.error(f"Ошибка при отправке ответа пользователю {target_user_id}: {e}")
await message.answer(f"Не удалось отправить сообщение пользователю. Ошибка: {e}")
await state.clear()

View file

@ -26,7 +26,7 @@ def get_main_menu(verified: bool = False, is_ig: bool = False, is_admin: bool =
) )
builder.row( builder.row(
InlineKeyboardButton(text='📋 Опросы', callback_data='polls'), InlineKeyboardButton(text='📋 Опросы', callback_data='polls'),
InlineKeyboardButton(text=' События', callback_data='events') InlineKeyboardButton(text='🎉 События', callback_data='events')
) )
builder.row( builder.row(
InlineKeyboardButton(text='🏪 Маркет', callback_data='market'), InlineKeyboardButton(text='🏪 Маркет', callback_data='market'),

0
nohup.out Normal file
View file

View file

@ -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"
}
]

View file

@ -16,4 +16,4 @@ services:
- TZ=Europe/Ulyanovsk - TZ=Europe/Ulyanovsk
- TELEGRAM_BOT_TOKEN=8725618164:AAH1tGalq-pw1l0t4P5c0sdLkCVJdh7IE7M - TELEGRAM_BOT_TOKEN=8725618164:AAH1tGalq-pw1l0t4P5c0sdLkCVJdh7IE7M
- TELEGRAM_CHAT_ID=197957361 - TELEGRAM_CHAT_ID=197957361
command: ["./start.sh", "ultimate"] command: ["./start.sh", "interactive"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

View file

@ -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")

View file

@ -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-файлов!")

View file

@ -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("✅ Сессия сохранена, браузер закрыт.")

View file

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