"""
МАРКЕТ ЛКМ37 (v2.0)
Доска объявлений с поддержкой фото и удобной навигацией.
"""
import logging
from datetime import datetime, timedelta
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, InputMediaPhoto
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 InlineKeyboardMarkup, 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"))
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)
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 == "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, 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)
)
session.add(new_ad)
await session.commit()
await message.answer(
f"{fmt_msg('ЛОТ СОЗДАН', 'system')}\n\n"
"Ваше объявление отправлено на модерацию. После одобрения оно появится в общем списке."
)
@router.callback_query(F.data == "ads_view_all")
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).offset(page).limit(1)
res = await session.execute(stmt)
ad = res.scalar_one_or_none()
if not ad:
return await callback.message.answer("К сожалению, пока лотов нет.")
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"))
kb.row(
InlineKeyboardButton(text="⬅️", callback_data=f"ad_page_{page-1}"),
InlineKeyboardButton(text="➡️", callback_data=f"ad_page_{page+1}")
)
...
@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: pass
await callback.answer("✅ Товар забронирован. Свяжитесь с продавцом!", show_alert=True)
await callback.message.edit_reply_markup(reply_markup=None) # Обновить бы на "Забронировано"
else:
await callback.answer("❌ Товар уже забронирован или не найден.")
kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="ads_back"))
if ad.photo_file_id:
await callback.message.answer_photo(ad.photo_file_id, caption=text, reply_markup=kb.as_markup())
else:
await callback.message.answer(text, reply_markup=kb.as_markup())
await callback.answer()