""" Доска объявлений — куплю, продам, отдам, найду Для всех верифицированных пользователей """ import logging from datetime import datetime, timedelta from aiogram import Router, F from aiogram.types import Message, CallbackQuery from aiogram.filters import Command, StateFilter from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.context import FSMContext from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from database.db import AsyncSessionLocal from database.models import Ad, User from utils.decorators import admin_only from keyboards.inline import get_ads_category_menu, get_ads_item_menu import config logger = logging.getLogger(__name__) router = Router() # ============================================================================= # FSM для создания объявления # ============================================================================= class AdState(StatesGroup): waiting_for_category = State() waiting_for_title = State() waiting_for_description = State() waiting_for_price = State() waiting_for_phone = State() # ============================================================================= # СОЗДАНИЕ ОБЪЯВЛЕНИЯ # ============================================================================= @router.message(Command('ad')) @router.message(Command('объявление')) async def cmd_ad_create(message: Message, state: FSMContext): """ Создать объявление Использование: /ad или /объявление """ # Проверяем верификацию async with AsyncSessionLocal() as session: user = await session.get(User, message.from_user.id) if not user or not user.verified: await message.answer( '❌ Только для верифицированных жильцов!\n\n' 'Сначала пройдите верификацию по номеру квартиры.\n' 'Напишите боту в личку номер вашей квартиры.', parse_mode='HTML' ) return await message.answer( '📢 Создание объявления\n\n' 'Выберите категорию:\n\n' '1️⃣ 💰 Продам\n' '2️⃣ 🛒 Куплю\n' '3️⃣ 🎁 Отдам\n' '4️⃣ 😿 Потеряно\n' '5️⃣ 😺 Нашлось\n' '6️⃣ 🔧 Услуга\n\n' 'Или отправьте номер категории (1-6):', reply_markup=get_ads_category_menu(), parse_mode='HTML' ) await state.set_state(AdState.waiting_for_category) @router.callback_query(F.data.startswith('ads_category_')) async def callback_ads_category(callback: CallbackQuery, state: FSMContext): """Выбор категории объявления""" category_map = { 'ads_category_sell': ('sell', '💰 Продам'), 'ads_category_buy': ('buy', '🛒 Куплю'), 'ads_category_give': ('give', '🎁 Отдам'), 'ads_category_lost': ('lost', '😿 Потеряно'), 'ads_category_found': ('found', '😺 Нашлось'), 'ads_category_service': ('service', '🔧 Услуга'), } cat_key, cat_name = category_map.get(callback.data, ('sell', '💰 Продам')) await state.update_data(category=cat_key, category_name=cat_name) await callback.message.answer( f'✅ Категория: {cat_name}\n\n' 'Теперь отправьте название объявления:\n' 'Например: "Продам диван"', parse_mode='HTML' ) await state.set_state(AdState.waiting_for_title) await callback.answer() @router.message(AdState.waiting_for_category) async def process_ads_category(message: Message, state: FSMContext): """Выбор категории текстом""" category_map = { '1': ('sell', '💰 Продам'), '2': ('buy', '🛒 Куплю'), '3': ('give', '🎁 Отдам'), '4': ('lost', '😿 Потеряно'), '5': ('found', '😺 Нашлось'), '6': ('service', '🔧 Услуга'), } text = message.text.strip() if text in category_map: cat_key, cat_name = category_map[text] await state.update_data(category=cat_key, category_name=cat_name) await message.answer( f'✅ Категория: {cat_name}\n\n' 'Теперь отправьте название объявления:\n' 'Например: "Продам диван"', parse_mode='HTML' ) await state.set_state(AdState.waiting_for_title) else: await message.answer('❌ Неверная категория. Выберите из меню или отправьте номер 1-6:') @router.message(AdState.waiting_for_title) async def process_ads_title(message: Message, state: FSMContext): """Получили название объявления""" title = message.text await state.update_data(title=title) await message.answer( f'✅ Название: {title}\n\n' 'Теперь отправьте описание:\n' 'Например: "Диван угловой, состояние хорошее, самовывоз"', parse_mode='HTML' ) await state.set_state(AdState.waiting_for_description) @router.message(AdState.waiting_for_description) async def process_ads_description(message: Message, state: FSMContext): """Получили описание""" description = message.text await state.update_data(description=description) await message.answer( f'✅ Описание принято\n\n' 'Укажите цену (в рублях) или отправьте "0" если бесплатно:\n' 'Например: 5000', parse_mode='HTML' ) await state.set_state(AdState.waiting_for_price) @router.message(AdState.waiting_for_price) async def process_ads_price(message: Message, state: FSMContext): """Получили цену""" text = message.text.strip() try: price = int(text) if price < 0: raise ValueError except ValueError: await message.answer('❌ Неверный формат цены. Отправьте число или "0":') return await state.update_data(price=price if price > 0 else None) await message.answer( f'✅ Цена: {price if price > 0 else "Бесплатно"}\n\n' 'Укажите контактный телефон или отправьте "пропустить":\n' 'Например: +7 (999) 123-45-67', parse_mode='HTML' ) await state.set_state(AdState.waiting_for_phone) @router.message(AdState.waiting_for_phone) async def process_ads_phone(message: Message, state: FSMContext): """Получили телефон или пропустили""" text = message.text.strip() if text.lower() == 'пропустить': phone = None else: phone = text await state.update_data(phone=phone) # Сохраняем объявление в БД data = await state.get_data() expires_at = datetime.utcnow() + timedelta(days=7) async with AsyncSessionLocal() as session: ad = Ad( user_id=message.from_user.id, category=data['category'], title=data['title'], description=data['description'], price=data.get('price'), phone=phone, expires_at=expires_at, ) session.add(ad) await session.commit() await session.refresh(ad) # Формируем объявление для публикации emoji = ad.get_category_emoji() cat_name = ad.get_category_name() price_text = f'{ad.price} ₽' if ad.price else 'Бесплатно' ad_text = ( f'{emoji} {cat_name}: {ad.title}\n\n' f'📝 {ad.description}\n\n' f'💰 Цена: {price_text}\n' ) if phone: ad_text += f'📞 Телефон: {phone}\n' ad_text += f'\n⏳ Действует до: {expires_at.strftime("%d.%m.%Y")}\n' ad_text += f'🔖 ID: {ad.ad_id}' # Отправляем в чат try: sent_message = await message.bot.send_message( config.ADMIN_CHAT_ID, ad_text, parse_mode='HTML' ) # Сохраняем message_id ad.message_id = sent_message.message_id await session.commit() except Exception as e: logger.error(f'Не удалось опубликовать объявление: {e}') await message.answer( f'✅ Объявление опубликовано!\n\n' f'ID: {ad.ad_id}\n' f'Действует до: {expires_at.strftime("%d.%m.%Y")}\n\n' f'Для просмотра: /ads', parse_mode='HTML' ) await state.clear() # ============================================================================= # ПРОСМОТР ОБЪЯВЛЕНИЙ # ============================================================================= @router.message(Command('ads')) @router.message(Command('доска')) async def cmd_ads_list(message: Message): """Показать все активные объявления""" async with AsyncSessionLocal() as session: # Получаем активные объявления stmt = ( select(Ad) .where(Ad.is_active == True) .where(Ad.expires_at > datetime.utcnow()) .order_by(Ad.created_at.desc()) .limit(20) ) result = await session.execute(stmt) ads = list(result.scalars().all()) if not ads: await message.answer('✅ Нет активных объявлений') return text = '📢 Доска объявлений\n\n' for i, ad in enumerate(ads, 1): emoji = ad.get_category_emoji() cat_name = ad.get_category_name() price_text = f'{ad.price} ₽' if ad.price else 'Бесплатно' text += f'{i}. {emoji} {cat_name}: {ad.title[:30]}\n' text += f' └─ Цена: {price_text}\n' text += f' └─ Действует до: {ad.expires_at.strftime("%d.%m.%Y")}\n' text += f' └─ ID: {ad.ad_id}\n\n' text += '💡 Для создания: /ad' await message.answer(text, parse_mode='HTML') # ============================================================================= # УДАЛЕНИЕ ОБЪЯВЛЕНИЯ # ============================================================================= @router.message(Command('ad_del')) async def cmd_ad_del(message: Message): """ Удалить своё объявление Использование: /ad_del """ args = message.text.split() if len(args) < 2: await message.answer( '❌ Использование:\n' '/ad_del <ID>\n\n' 'ID можно узнать из /ads', parse_mode='HTML' ) return try: ad_id = int(args[1]) except ValueError: await message.answer('❌ Неверный ID') return async with AsyncSessionLocal() as session: ad = await session.get(Ad, ad_id) if not ad: await message.answer(f'❌ Объвление {ad_id} не найдено') return # Проверяем что это своё объявление или админ if ad.user_id != message.from_user.id and not config.is_admin(message.from_user.id): await message.answer('❌ Можно удалять только свои объявления!') return ad.is_active = False await session.commit() # Пытаемся удалить сообщение из чата if ad.message_id: try: await message.bot.delete_message(config.ADMIN_CHAT_ID, ad.message_id) except Exception: pass await message.answer(f'✅ Объявление "{ad.title}" удалено') # ============================================================================= # МОДЕРАЦИЯ (для админов) # ============================================================================= @router.message(Command('ads_moderate')) @admin_only async def cmd_ads_moderate(message: Message): """Показать объявления на модерации""" async with AsyncSessionLocal() as session: stmt = ( select(Ad) .where(Ad.is_moderated == False) .where(Ad.is_active == True) .order_by(Ad.created_at.desc()) ) result = await session.execute(stmt) ads = list(result.scalars().all()) if not ads: await message.answer('✅ Нет объявлений на модерации') return text = '📋 Объявления на модерации\n\n' for ad in ads: user = await session.get(User, ad.user_id) text += f'• ID {ad.ad_id}: {ad.title} (от {user.full_name if user else "?"})\n' text += '\n💡 Для подтверждения: /ad_approve ' await message.answer(text) @router.message(Command('ad_approve')) @admin_only async def cmd_ad_approve(message: Message): """ Подтвердить объявление Использование: /ad_approve """ args = message.text.split() if len(args) < 2: await message.answer('❌ Использование: /ad_approve ') return try: ad_id = int(args[1]) except ValueError: await message.answer('❌ Неверный ID') return async with AsyncSessionLocal() as session: ad = await session.get(Ad, ad_id) if not ad: await message.answer(f'❌ Объвление {ad_id} не найдено') return ad.is_moderated = True await session.commit() await message.answer(f'✅ Объявление {ad_id} подтверждено') # ============================================================================= # АВТО-УДАЛЕНИЕ СТАРЫХ ОБЪЯВЛЕНИЙ # ============================================================================= async def cleanup_expired_ads(): """Удалить просроченные объявления""" async with AsyncSessionLocal() as session: stmt = ( select(Ad) .where(Ad.is_active == True) .where(Ad.expires_at <= datetime.utcnow()) ) result = await session.execute(stmt) ads = list(result.scalars().all()) for ad in ads: ad.is_active = False # Пытаемся удалить сообщение if ad.message_id: try: from aiogram import Bot # Bot будет передан из scheduler except Exception: pass await session.commit() if ads: logger.info(f'Удалено {len(ads)} просроченных объявлений')