Compare commits

..

2 commits

5 changed files with 79 additions and 58 deletions

View file

@ -130,6 +130,7 @@ async def cmd_legal_sync(message: Message):
@router.message(Command('admin'))
@router.message(F.text.in_(['⚙️ Управление', '⚙️ Админка', '⚙️ АДМИНКА']))
@admin_only
async def cmd_admin(message: Message):
"""Админ-меню (только в личке!)"""
@ -150,7 +151,7 @@ async def cmd_admin(message: Message):
)
@router.message(F.text == '📊 Статистика')
@router.message(F.text.in_(['📊 Статистика', '/stats']))
@admin_only
async def msg_stats_button(message: Message):
"""Кнопка Статистика"""
@ -162,19 +163,19 @@ async def msg_stats_button(message: Message):
await message.answer(text, parse_mode='HTML')
@router.message(F.text == '🕵️ Шпионы')
@router.message(F.text.in_(['🛡️ Фильтр посторонних', '🕵️ Шпионы', '/spies']))
@admin_only
async def msg_spies_button(message: Message):
"""Кнопка Шпионы"""
"""Кнопка Шпионы / Фильтр посторонних"""
async with AsyncSessionLocal() as session:
spy_detector = SpyDetector(session)
spies = await spy_detector.get_spies()
if not spies:
await message.answer('🟢 Шпионов не обнаружено')
await message.answer('🟢 Посторонних и шпионов не обнаружено')
return
text = '🚨 <b>ШПИОНЫ</b> 🚨\n\n'
text = '🚨 <b>ПОСТОРОННИЕ / ШПИОНЫ</b> 🚨\n\n'
for spy in spies:
name = spy.full_name or f'user{spy.user_id}'
apt = spy.apartment or '?'
@ -183,16 +184,16 @@ async def msg_spies_button(message: Message):
await message.answer(text, parse_mode='HTML')
@router.message(F.text == '👥 Пользователи')
@router.message(F.text.in_(['👥 Список жителей', '👥 Пользователи', '/users']))
@admin_only
async def msg_users_button(message: Message):
"""Кнопка Пользователи"""
"""Кнопка Пользователи / Список жителей"""
async with AsyncSessionLocal() as session:
stmt = select(User).limit(50)
result = await session.execute(stmt)
users = list(result.scalars().all())
text = f'👥 <b>Пользователи</b> (50):\n\n'
text = f'👥 <b>Список жителей</b> (50):\n\n'
for user in users:
status = '✅' if user.verified else '⏳'
name = user.full_name or f'user{user.user_id}'
@ -204,7 +205,7 @@ async def msg_users_button(message: Message):
await message.answer(text, parse_mode='HTML')
@router.message(F.text == '❓ Помощь')
@router.message(F.text.in_(['❓ Справка', '❓ Помощь', '/help']), lambda m: config.is_admin(m.from_user.id))
@admin_only
async def msg_help_button(message: Message):
"""Кнопка Помощь - справка по командам"""
@ -699,6 +700,7 @@ async def cmd_spies(message: Message):
@router.message(Command('export'))
@router.message(Command('экспорт'))
@router.message(F.text.in_(['📥 Выгрузка базы', '📥 Экспорт JSON']))
@admin_only
async def cmd_export(message: Message):
"""Экспорт данных"""

View file

@ -110,6 +110,7 @@ async def cb_main_menu(callback: CallbackQuery):
)
@router.message(Command('phones'))
@router.message(F.text.in_(['📞 Важные телефоны', '📞 Телефоны', 'Телефоны']))
@router.callback_query(F.data == 'phones')
async def cb_phones(event):
text = '📞 <b>ЭКСТРЕННЫЕ ТЕЛЕФОНЫ</b>\n\nВыберите категорию ниже:'
@ -117,7 +118,7 @@ async def cb_phones(event):
await safe_edit_or_reply(event, text, reply_markup=kb, parse_mode='HTML')
@router.message(Command('masters'))
@router.message(F.text.lower() == 'мастера')
@router.message(F.text.in_(['👷 Мастера', 'Мастера', 'мастера']))
async def cmd_masters(message: Message):
"""Выдача списка мастеров по команде /masters или тексту 'мастера'"""
from database.models import Service
@ -334,6 +335,7 @@ class HelpFeedbackState(StatesGroup):
waiting_for_reply = State()
@router.message(Command('help'))
@router.message(F.text.in_(['❓ Справка', '❓ Помощь', 'Справка', 'Помощь']))
@router.callback_query(F.data == 'help')
async def cb_help(event, state: FSMContext):
await state.clear()

View file

@ -14,53 +14,52 @@ def get_main_menu(verified: bool = False, is_ig: bool = False, is_admin: bool =
# СТРОКА 1: БАЗА (ВСЕГДА ТУТ)
builder.row(
InlineKeyboardButton(text='📞 Телефоны', callback_data='phones'),
InlineKeyboardButton(text='📞 Важные телефоны', callback_data='phones'),
InlineKeyboardButton(text='⚖️ Юрист (ЖК РФ)', callback_data='main_jurist')
)
builder.row(
InlineKeyboardButton(text='🏢 О доме', callback_data='about_house')
InlineKeyboardButton(text='🏢 Паспорт дома', callback_data='about_house')
)
# СТРОКА 2-3: ДЛЯ ВЕРИФИЦИРОВАННЫХ (ФИКСИРОВАННОЕ МЕСТО)
if verified:
builder.row(
InlineKeyboardButton(text='📅 График', callback_data='schedule'),
InlineKeyboardButton(text='💰 Платежи', callback_data='payments')
InlineKeyboardButton(text='⚠️ Отключения', callback_data='schedule'),
InlineKeyboardButton(text='💳 Оплата ЖКХ', callback_data='payments')
)
builder.row(
InlineKeyboardButton(text='📋 Опросы', callback_data='polls'),
InlineKeyboardButton(text='🎉 События', callback_data='events')
InlineKeyboardButton(text='🗳️ Голосования', callback_data='polls'),
InlineKeyboardButton(text='🗓️ События дома', callback_data='events')
)
builder.row(
InlineKeyboardButton(text='🏪 Маркет', callback_data='market'),
InlineKeyboardButton(text='🏆 Рейтинг', callback_data='achievements')
InlineKeyboardButton(text='📢 Объявления', callback_data='market'),
InlineKeyboardButton(text='🤝 Добрые соседи', callback_data='achievements')
)
else:
# Для гостей кнопка верификации на видном месте
builder.row(
InlineKeyboardButton(text='🛡️ ВЕРИФИКАЦИЯ', callback_data='start_verification')
InlineKeyboardButton(text='🔑 Подтвердить квартиру', callback_data='start_verification')
)
# СТРОКА 4: ИНИЦИАТИВНАЯ ГРУППА И АДМИН
if is_ig or is_admin:
buttons = []
if is_ig:
buttons.append(InlineKeyboardButton(text='🎯 ТИМ-МЕНЮ', callback_data='ig_menu'))
buttons.append(InlineKeyboardButton(text='👥 Совет Дома', callback_data='ig_menu'))
if is_admin:
buttons.append(InlineKeyboardButton(text='⚙️ АДМИНКА', callback_data='admin_menu'))
buttons.append(InlineKeyboardButton(text='⚙️ Управление', callback_data='admin_menu'))
builder.row(*buttons)
# СТРОКА 5: СУПЕР-АДМИН (ТЫ)
# СТРОКА 5: СУПЕР-АДМИН (ПРЕДСЕДАТЕЛЬ)
if is_superadmin:
builder.row(
InlineKeyboardButton(text='👑 [ROOT: УПРАВЛЕНИЕ]', callback_data='superadmin_menu')
InlineKeyboardButton(text='🛡️ Панель председателя', callback_data='superadmin_menu')
)
# СТРОКА 6: СЛУЖЕБНОЕ (НИЗ)
builder.row(
InlineKeyboardButton(text='❓ Помощь', callback_data='help'),
InlineKeyboardButton(text='🌦️ Погода', callback_data='weather'),
InlineKeyboardButton(text='👤 Профиль', callback_data='my_profile')
InlineKeyboardButton(text='❓ Справка', callback_data='help'),
InlineKeyboardButton(text='👤 Моя квартира', callback_data='my_profile')
)
return builder.as_markup()
@ -124,14 +123,14 @@ def get_admin_menu() -> InlineKeyboardMarkup:
builder.row(
InlineKeyboardButton(text='📊 Статистика', callback_data='admin_stats'),
InlineKeyboardButton(text='🕵️ Шпионы', callback_data='admin_spies'),
InlineKeyboardButton(text='🛡️ Фильтр посторонних', callback_data='admin_spies'),
)
builder.row(
InlineKeyboardButton(text='👥 Пользователи', callback_data='admin_users'),
InlineKeyboardButton(text='⏳ Непроверенные', callback_data='admin_unverified'),
InlineKeyboardButton(text='👥 Список жителей', callback_data='admin_users'),
InlineKeyboardButton(text='⏳ Ожидают проверки', callback_data='admin_unverified'),
)
builder.row(
InlineKeyboardButton(text='📥 Экспорт JSON', callback_data='admin_export'),
InlineKeyboardButton(text='📥 Выгрузка базы', callback_data='admin_export'),
InlineKeyboardButton(text='📤 Рассылка', callback_data='admin_broadcast'),
)
builder.row(

View file

@ -13,11 +13,11 @@ def get_reply_menu() -> ReplyKeyboardMarkup:
builder = ReplyKeyboardBuilder()
builder.row(
KeyboardButton(text='📞 Телефоны'),
KeyboardButton(text='📞 Важные телефоны'),
KeyboardButton(text='👷 Мастера'),
)
builder.row(
KeyboardButton(text='❓ Помощь'),
KeyboardButton(text='❓ Справка'),
)
# Resize keyboard чтобы не занимал много места
@ -29,12 +29,13 @@ def get_admin_reply_menu() -> ReplyKeyboardMarkup:
builder = ReplyKeyboardBuilder()
builder.row(
KeyboardButton(text='/admin'),
KeyboardButton(text='/stats'),
KeyboardButton(text='⚙️ Управление'),
KeyboardButton(text='📊 Статистика'),
)
builder.row(
KeyboardButton(text='/spies'),
KeyboardButton(text='/export'),
KeyboardButton(text='🛡️ Фильтр посторонних'),
KeyboardButton(text='📥 Выгрузка базы'),
)
return builder.as_markup(resize_keyboard=True)

View file

@ -21,7 +21,7 @@ class Scheduler:
def __init__(self, bot):
self.bot = bot
self.scheduler = AsyncIOScheduler()
self.scheduler = AsyncIOScheduler(timezone='Europe/Ulyanovsk')
def start(self):
"""Запуск планировщика"""
@ -141,25 +141,24 @@ class Scheduler:
)
logger.info('✅ Добавлена задача ежедневного прогноза погоды для админа')
# Еженедельный прогноз в чат по понедельникам (в 09:00)
self.scheduler.add_job(
self.monday_chat_weather,
CronTrigger.from_crontab('0 9 * * 1'), # Пн в 09:00
id='monday_chat_weather',
name='Прогноз погоды в чат (Понедельник)',
replace_existing=True
)
logger.info('✅ Добавлена задача прогноза погоды в чат по понедельникам')
# Еженедельный прогноз в чат по четвергам (в 09:00)
self.scheduler.add_job(
self.thursday_chat_weather,
CronTrigger.from_crontab('0 9 * * 4'), # Чт в 09:00
id='thursday_chat_weather',
name='Прогноз погоды в чат (Четверг)',
replace_existing=True
)
logger.info('✅ Добавлена задача прогноза погоды в чат по четвергам')
# [ОТКЛЮЧЕНО ПО ТЗ] Еженедельный прогноз в чат по понедельникам и четвергам отключен для группы дома
# Жильцы начали активнее общаться, прогноз погоды в общей группе больше не требуется.
# Личный прогноз для Александра (daily_admin_weather) выше остается активным.
# self.scheduler.add_job(
# self.monday_chat_weather,
# CronTrigger.from_crontab('0 9 * * 1'),
# id='monday_chat_weather',
# name='Прогноз погоды в чат (Понедельник)',
# replace_existing=True
# )
# self.scheduler.add_job(
# self.thursday_chat_weather,
# CronTrigger.from_crontab('0 9 * * 4'),
# id='thursday_chat_weather',
# name='Прогноз погоды в чат (Четверг)',
# replace_existing=True
# )
logger.info('ℹ️ Прогноз погоды в общий чат дома отключен по требованию председателя')
# Еженедельный дайджест Александра (суббота в 08:00)
self.scheduler.add_job(
@ -550,8 +549,17 @@ class Scheduler:
"""Прогноз в чат по понедельникам (на 3 дня)"""
try:
from services.weather import WeatherService
from config import ADMIN_CHAT_ID
from config import ADMIN_CHAT_ID, ADMIN_USER_ID
forecast = await WeatherService.get_3day_forecast()
if not forecast or forecast.startswith("❌"):
logger.warning(f"Monday weather forecast failed: {forecast}")
await self.bot.send_message(
ADMIN_USER_ID,
f"⚠️ <b>Внимание:</b> Не удалось сгенерировать прогноз погоды на понедельник для общего чата:\n{forecast}",
parse_mode='HTML'
)
return
await self.bot.send_message(
ADMIN_CHAT_ID,
forecast,
@ -565,8 +573,17 @@ class Scheduler:
"""Прогноз в чат по четвергам (на остаток недели и выходные)"""
try:
from services.weather import WeatherService
from config import ADMIN_CHAT_ID
from config import ADMIN_CHAT_ID, ADMIN_USER_ID
forecast = await WeatherService.get_thursday_weekend_broadcast()
if not forecast or forecast.startswith("❌"):
logger.warning(f"Thursday weather forecast failed: {forecast}")
await self.bot.send_message(
ADMIN_USER_ID,
f"⚠️ <b>Внимание:</b> Не удалось сгенерировать прогноз погоды на четверг для общего чата:\n{forecast}",
parse_mode='HTML'
)
return
await self.bot.send_message(
ADMIN_CHAT_ID,
forecast,