"""
Доска объявлений — куплю, продам, отдам, найду
Для всех верифицированных пользователей
"""
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=14) # 14 дней вместо 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(
chat_id=config.ADMIN_CHAT_ID,
message_thread_id=config.get_topic_id('ads'), # Тема "Важные объявления"
text=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)} просроченных объявлений')