""" МАРКЕТ ЛКМ37 (v2.0) Доска объявлений с поддержкой фото и удобной навигацией. """ 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.ui_styles import fmt_msg from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.types import InlineKeyboardButton import config logger = logging.getLogger(__name__) router = Router() class AdState(StatesGroup): waiting_for_category = State() waiting_for_title = State() waiting_for_description = State() waiting_for_price = State() waiting_for_photo = State() # ============================================================================= # КЛАВИАТУРЫ # ============================================================================= def get_ads_main_menu(): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🛒 Все товары", callback_data="ads_view_all")) builder.row(InlineKeyboardButton(text="➕ Создать лот", callback_data="ads_create")) builder.row(InlineKeyboardButton(text="👤 Мои лоты", callback_data="ads_my_items")) builder.row(InlineKeyboardButton(text="🏠 В главное меню", callback_data="main_menu")) return builder.as_markup() def get_categories_kb(): builder = InlineKeyboardBuilder() cats = [ ("Продам 💰", "sell"), ("Куплю 🛒", "buy"), ("Отдам 🎁", "give"), ("Услуги 🔧", "service") ] for name, code in cats: builder.add(InlineKeyboardButton(text=name, callback_data=f"adcat_{code}")) builder.adjust(2) builder.row(InlineKeyboardButton(text="🔙 В маркет", callback_data="ads_back")) return builder.as_markup() # ============================================================================= # ХЕНДЛЕРЫ # ============================================================================= @router.message(Command("market")) async def cmd_market(message: Message): """Главный вход в маркет""" text = ( f"{fmt_msg('МАРКЕТ ЛКМ37', 'system')}\n\n" "Здесь вы можете найти товары от соседей или предложить свои.\n" "Все лоты проходят модерацию." ) 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" "Все лоты проходят модерацию." ) 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") async def start_ad_creation(callback: CallbackQuery, state: FSMContext): await callback.message.answer("Выберите категорию для вашего лота:", reply_markup=get_categories_kb()) await state.set_state(AdState.waiting_for_category) await callback.answer() @router.callback_query(AdState.waiting_for_category, F.data.startswith("adcat_")) async def process_cat(callback: CallbackQuery, state: FSMContext): cat = callback.data.split("_")[1] await state.update_data(category=cat) await callback.message.answer("Введите краткое название (заголовок) товара:") await state.set_state(AdState.waiting_for_title) await callback.answer() @router.message(AdState.waiting_for_title) async def process_title(message: Message, state: FSMContext): await state.update_data(title=message.text) await message.answer("Опишите товар подробнее (состояние, причина продажи и т.д.):") await state.set_state(AdState.waiting_for_description) @router.message(AdState.waiting_for_description) async def process_desc(message: Message, state: FSMContext): await state.update_data(description=message.text) await message.answer("Укажите цену в рублях (или 0 если даром):") await state.set_state(AdState.waiting_for_price) @router.message(AdState.waiting_for_price) async def process_price(message: Message, state: FSMContext): if not message.text.isdigit(): return await message.answer("Пожалуйста, введите только число.") await state.update_data(price=int(message.text)) await message.answer("Пришлите фото товара (или нажмите /skip если нет фото):") await state.set_state(AdState.waiting_for_photo) @router.message(AdState.waiting_for_photo, F.photo) async def process_photo(message: Message, state: FSMContext): photo_id = message.photo[-1].file_id data = await state.get_data() await save_ad(message, data, photo_id) await state.clear() @router.message(AdState.waiting_for_photo, Command("skip")) async def skip_photo(message: Message, state: FSMContext): data = await state.get_data() await save_ad(message, data, None) await state.clear() async def save_ad(message: Message, data, photo_id): async with AsyncSessionLocal() as session: new_ad = Ad( user_id=message.from_user.id, category=data['category'], title=data['title'], description=data['description'], price=data['price'], photo_file_id=photo_id, expires_at=datetime.utcnow() + timedelta(days=30), is_moderated=True ) session.add(new_ad) await session.commit() await message.answer( f"{fmt_msg('ЛОТ СОЗДАН', 'system')}\n\n" "Ваше объявление успешно создано и опубликовано в маркете LKM37!" ) @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 with AsyncSessionLocal() as session: 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) ad = res.scalar_one_or_none() if not ad: await callback.answer("К сожалению, пока лотов нет.", show_alert=True) return price_text = f"{ad.price} ₽" if ad.price > 0 else "ДАРOM" text = ( f"{ad.title}\n\n" f"💰 Цена: {price_text}\n" f"📝 {ad.description}\n\n" f"👤 Продавец: ID{ad.user_id}" ) kb = InlineKeyboardBuilder() kb.row(InlineKeyboardButton(text="📧 Написать продавцу", url=f"tg://user?id={ad.user_id}")) if not ad.reserved_by: kb.row(InlineKeyboardButton(text="🤝 БРОНИРОВАТЬ", callback_data=f"ad_reserve_{ad.ad_id}")) else: kb.row(InlineKeyboardButton(text="🤝 ЗАБРОНИРОВАНО", callback_data="ad_reserved_info")) # Пагинация nav_buttons = [] 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_")) async def reserve_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 not ad.reserved_by: ad.reserved_by = callback.from_user.id await session.commit() # Уведомляем продавца try: await callback.bot.send_message( ad.user_id, f"🤝 Ваш товар забронирован!\n\n" f"Товар: {ad.title}\n" f"Покупатель: ID{callback.from_user.id} (@{callback.from_user.username or '—'})", parse_mode='HTML' ) except Exception: pass await callback.answer("✅ Товар забронирован. Свяжитесь с продавцом!", show_alert=True) # Обновим клавиатуру 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: await callback.answer("❌ Товар уже забронирован или не найден.", show_alert=True) @router.callback_query(F.data == "ad_reserved_info") async def cb_reserved_info(callback: CallbackQuery): await callback.answer("Этот товар уже забронирован кем-то из соседей.", show_alert=True) @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 = "👤 ВАШИ ОБЪЯВЛЕНИЯ:\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"🔹 {ad.title} ({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() @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)