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}"
)