"""
МАРКЕТ ЛКМ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)