import re from datetime import datetime import pytz from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, Message from sqlalchemy import delete, select, update from sqlalchemy.ext.asyncio import AsyncSession from config import USERNAME_BOT from core.cache_config import START_UTM_EXISTS_TTL_SEC from core.redis_cache import cache_delete, cache_key, cache_set from database import create_tracking_source, get_tracking_source_stats from database.models import TrackingSource, User from filters.admin import IsAdminFilter from logger import logger from ..panel.keyboard import AdminPanelCallback from .keyboard import ( AdminAdsCallback, build_ads_delete_confirm_kb, build_ads_kb, build_ads_list_kb, build_ads_stats_kb, build_cancel_input_kb, ) from filters.admin import HasPermission from filters.permissions import PERM_ADS router = Router() router.callback_query.filter(HasPermission(PERM_ADS)) router.message.filter(HasPermission(PERM_ADS)) class AdminAdsState(StatesGroup): waiting_for_new_name = State() waiting_for_new_code = State() @router.callback_query(AdminPanelCallback.filter(F.action == "ads"), IsAdminFilter()) async def handle_ads_menu(callback_query: CallbackQuery): await callback_query.message.edit_text(text="πŸ“Š Аналитика Ρ€Π΅ΠΊΠ»Π°ΠΌΡ‹:", reply_markup=build_ads_kb()) @router.callback_query(AdminAdsCallback.filter(F.action == "create"), IsAdminFilter()) async def handle_ads_create(callback_query: CallbackQuery, state: FSMContext): await state.set_state(AdminAdsState.waiting_for_new_name) await callback_query.message.edit_text( "πŸ“ Π’Π²Π΅Π΄ΠΈΡ‚Π΅ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ Π½ΠΎΠ²ΠΎΠΉ ссылки:", reply_markup=build_cancel_input_kb() ) @router.message(AdminAdsState.waiting_for_new_name, IsAdminFilter()) async def handle_ads_name_input(message: Message, state: FSMContext): name = message.text.strip() await state.update_data(name=name) await state.set_state(AdminAdsState.waiting_for_new_code) await message.answer( f"πŸ”— Π’Π²Π΅Π΄ΠΈΡ‚Π΅ ΠΊΠΎΠ΄ ссылки для: {name}.\n\nπŸ’‘ ΠŸΡ€Π°Π²ΠΈΠ»Π°: Ρ‚ΠΎΠ»ΡŒΠΊΠΎ латинскиС Π±ΡƒΠΊΠ²Ρ‹ ΠΈ Ρ†ΠΈΡ„Ρ€Ρ‹", reply_markup=build_cancel_input_kb(), ) @router.message(AdminAdsState.waiting_for_new_code, IsAdminFilter()) async def handle_ads_code_input(message: Message, state: FSMContext, session: AsyncSession): code = message.text.strip() data = await state.get_data() name = data["name"] if not re.match(r"^[a-zA-Z0-9]+$", code): await message.answer( "❌ Код ΠΌΠΎΠΆΠ΅Ρ‚ ΡΠΎΠ΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ латинскиС Π±ΡƒΠΊΠ²Ρ‹ ΠΈ Ρ†ΠΈΡ„Ρ€Ρ‹\nΠ’Π²Π΅Π΄ΠΈΡ‚Π΅ ΠΊΠΎΠ΄ Π·Π°Π½ΠΎΠ²ΠΎ:", reply_markup=build_cancel_input_kb(), ) return code_with_prefix = f"utm_{code}" try: await create_tracking_source( name=name, code=code_with_prefix, type_="utm", created_by=message.from_user.id, session=session, ) await cache_set(cache_key("utm_exists", code_with_prefix), True, START_UTM_EXISTS_TTL_SEC) stats = await get_tracking_source_stats(session, code_with_prefix) if not stats: await message.answer("❌ Π˜ΡΡ‚ΠΎΡ‡Π½ΠΈΠΊ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ ΠΈΠ»ΠΈ Π½Π΅ содСрТит Π΄Π°Π½Π½Ρ‹Ρ….") return msg = format_ads_stats(stats, USERNAME_BOT) await message.answer( text=msg, reply_markup=build_ads_stats_kb(code_with_prefix), ) except Exception as e: logger.error(f"Ошибка ΠΏΡ€ΠΈ создании ссылки: {e}", exc_info=True) await message.answer("❌ ΠŸΡ€ΠΎΠΈΠ·ΠΎΡˆΠ»Π° ошибка ΠΏΡ€ΠΈ создании ссылки.") finally: await state.clear() @router.callback_query(AdminAdsCallback.filter(F.action == "list"), IsAdminFilter()) async def handle_ads_list(callback_query: CallbackQuery, session: AsyncSession, callback_data: AdminAdsCallback): try: result = await session.execute(select(TrackingSource).order_by(TrackingSource.created_at.desc())) ads = result.scalars().all() items_per_page = 6 if callback_data.code and callback_data.code.isdigit(): current_page = int(callback_data.code) else: current_page = 1 total_pages = (len(ads) + items_per_page - 1) // items_per_page reply_markup = build_ads_list_kb(ads, current_page, total_pages) await callback_query.message.edit_text( "πŸ“‹ Π’Ρ‹Π±Π΅Ρ€ΠΈΡ‚Π΅ ссылку для просмотра статистики:", reply_markup=reply_markup ) except Exception as e: logger.error(f"Ошибка ΠΏΡ€ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠΈ списка UTM: {e}", exc_info=True) await callback_query.message.edit_text("❌ ΠŸΡ€ΠΎΠΈΠ·ΠΎΡˆΠ»Π° ошибка ΠΏΡ€ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠΈ списка.") @router.callback_query(AdminAdsCallback.filter(F.action == "view"), IsAdminFilter()) async def handle_ads_view( callback_query: CallbackQuery, callback_data: AdminAdsCallback, session: AsyncSession, ): code = callback_data.code try: stats = await get_tracking_source_stats(session, code) if not stats: await callback_query.message.edit_text("❌ Π˜ΡΡ‚ΠΎΡ‡Π½ΠΈΠΊ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ ΠΈΠ»ΠΈ Π½Π΅ содСрТит Π΄Π°Π½Π½Ρ‹Ρ….") return msg = format_ads_stats(stats, USERNAME_BOT) await callback_query.message.edit_text(text=msg, reply_markup=build_ads_stats_kb(code)) except Exception as e: logger.error(f"Ошибка ΠΏΡ€ΠΈ просмотрС статистики: {e}", exc_info=True) await callback_query.message.edit_text("❌ Ошибка ΠΏΡ€ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠΈ статистики.") @router.callback_query(AdminAdsCallback.filter(F.action == "delete_confirm"), IsAdminFilter()) async def handle_ads_delete_confirm(callback_query: CallbackQuery, callback_data: AdminAdsCallback): code = callback_data.code await callback_query.message.edit_text( text=f"Π’Ρ‹ ΡƒΠ²Π΅Ρ€Π΅Π½Ρ‹, Ρ‡Ρ‚ΠΎ Ρ…ΠΎΡ‚ΠΈΡ‚Π΅ ΡƒΠ΄Π°Π»ΠΈΡ‚ΡŒ ссылку {code}?", reply_markup=build_ads_delete_confirm_kb(code), ) @router.callback_query(AdminAdsCallback.filter(F.action == "delete"), IsAdminFilter()) async def handle_ads_delete( callback_query: CallbackQuery, callback_data: AdminAdsCallback, session: AsyncSession, ): code = callback_data.code try: await session.execute(update(User).where(User.source_code == code).values(source_code=None)) await session.execute(delete(TrackingSource).where(TrackingSource.code == code)) await cache_delete(cache_key("utm_exists", code)) await callback_query.message.edit_text( f"πŸ—‘οΈ Бсылка {code} ΡƒΠ΄Π°Π»Π΅Π½Π°.", reply_markup=build_ads_kb(), ) except Exception as e: logger.error(f"Ошибка ΠΏΡ€ΠΈ ΡƒΠ΄Π°Π»Π΅Π½ΠΈΠΈ ΠΌΠ΅Ρ‚ΠΊΠΈ {code}: {e}", exc_info=True) await callback_query.message.edit_text("❌ НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΡƒΠ΄Π°Π»ΠΈΡ‚ΡŒ ссылку.") @router.callback_query(AdminAdsCallback.filter(F.action == "cancel_input"), IsAdminFilter()) async def handle_ads_cancel_input(callback_query: CallbackQuery, state: FSMContext): await state.clear() await callback_query.message.edit_text(text="πŸ“Š Аналитика Ρ€Π΅ΠΊΠ»Π°ΠΌΡ‹:", reply_markup=build_ads_kb()) def format_ads_stats(stats: dict, username_bot: str) -> str: moscow_tz = pytz.timezone("Europe/Moscow") now = datetime.now(moscow_tz) update_time = now.strftime("%d.%m.%y %H:%M:%S") return ( f"πŸ“Š Бтатистика ΠΏΠΎ Ρ€Π΅ΠΊΠ»Π°ΠΌΠ½ΠΎΠΉ ссылкС\n\n" f"πŸ“Œ НазваниС: {stats['name']}\n" f"πŸ”— Бсылка: https://t.me/{username_bot}?start={stats['code']}\n" f"πŸ•“ Π‘ΠΎΠ·Π΄Π°Π½Π°: {stats['created_at'].strftime('%d.%m.%Y %H:%M')}\n\n" f"πŸ’‘ ΠΠΊΡ‚ΠΈΠ²Π½ΠΎΡΡ‚ΡŒ:\n" f"β”” πŸ†• РСгистраций: {stats.get('registrations', 0)}\n" f"β”” πŸ§ͺ Π’Ρ€ΠΈΠ°Π»ΠΎΠ²: {stats.get('trials', 0)}\n\n" f"πŸ’° Ѐинансовая информация:\n" f"β”œ πŸ’³ ΠŸΠΎΠΊΡƒΠΏΠΎΠΊ: {stats.get('payments', 0)}\n" f"β”” πŸ’Έ Π‘ΡƒΠΌΠΌΠ°: {round(stats.get('total_amount', 0), 2)} β‚½\n\n" f"⏱️ ПослСднСС ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅: {update_time}" )