feat: добавлена обратная связь с FSM в раздел Помощь, эмодзи в меню, исправлен маркет в боте
This commit is contained in:
parent
f9116f14e9
commit
a1ae78e5f8
16 changed files with 919 additions and 27 deletions
25
.dockerignore
Normal file
25
.dockerignore
Normal 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
|
||||||
143
handlers/ads.py
143
handlers/ads.py
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
0
nohup.out
Normal file
382
scraper_gis/cabinet_links_map.json
Normal file
382
scraper_gis/cabinet_links_map.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
BIN
scraper_gis/exports/01_Main_Dashboard.png
Normal file
BIN
scraper_gis/exports/01_Main_Dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 375 KiB |
BIN
scraper_gis/exports/02_Appeals_view.png
Normal file
BIN
scraper_gis/exports/02_Appeals_view.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 227 KiB |
BIN
scraper_gis/exports/02_Bills_view.png
Normal file
BIN
scraper_gis/exports/02_Bills_view.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 229 KiB |
BIN
scraper_gis/exports/02_Main_view.png
Normal file
BIN
scraper_gis/exports/02_Main_view.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 372 KiB |
BIN
scraper_gis/exports/02_Voting_view.png
Normal file
BIN
scraper_gis/exports/02_Voting_view.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 230 KiB |
87
scraper_gis/mcp_full_parser.py
Normal file
87
scraper_gis/mcp_full_parser.py
Normal 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")
|
||||||
88
scraper_gis/mcp_smart_parser.py
Normal file
88
scraper_gis/mcp_smart_parser.py
Normal 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-файлов!")
|
||||||
28
scraper_gis/mcp_test_auth.py
Normal file
28
scraper_gis/mcp_test_auth.py
Normal 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("✅ Сессия сохранена, браузер закрыт.")
|
||||||
54
scraper_gis/mcp_test_cabinet.py
Normal file
54
scraper_gis/mcp_test_cabinet.py
Normal 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()
|
||||||
Loading…
Reference in a new issue