diff --git a/app/handlers/admin/campaigns.py b/app/handlers/admin/campaigns.py
index 68708b67..482221fa 100644
--- a/app/handlers/admin/campaigns.py
+++ b/app/handlers/admin/campaigns.py
@@ -2,7 +2,7 @@ import logging
import re
from typing import List
-from aiogram import Dispatcher, types, F
+from aiogram import Bot, Dispatcher, types, F
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
@@ -24,6 +24,7 @@ from app.keyboards.admin import (
get_admin_campaigns_keyboard,
get_admin_pagination_keyboard,
get_campaign_bonus_type_keyboard,
+ get_campaign_edit_keyboard,
get_campaign_management_keyboard,
get_confirmation_keyboard,
)
@@ -78,7 +79,12 @@ async def _get_bot_deep_link_from_message(
def _build_campaign_servers_keyboard(
- servers, selected_uuids: List[str]
+ servers,
+ selected_uuids: List[str],
+ *,
+ toggle_prefix: str = "campaign_toggle_server_",
+ save_callback: str = "campaign_servers_save",
+ back_callback: str = "admin_campaigns",
) -> types.InlineKeyboardMarkup:
keyboard: List[List[types.InlineKeyboardButton]] = []
@@ -89,7 +95,7 @@ def _build_campaign_servers_keyboard(
keyboard.append(
[
types.InlineKeyboardButton(
- text=text, callback_data=f"campaign_toggle_server_{server.id}"
+ text=text, callback_data=f"{toggle_prefix}{server.id}"
)
]
)
@@ -97,15 +103,44 @@ def _build_campaign_servers_keyboard(
keyboard.append(
[
types.InlineKeyboardButton(
- text="✅ Сохранить", callback_data="campaign_servers_save"
+ text="✅ Сохранить", callback_data=save_callback
+ ),
+ types.InlineKeyboardButton(
+ text="⬅️ Назад", callback_data=back_callback
),
- types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_campaigns"),
]
)
return types.InlineKeyboardMarkup(inline_keyboard=keyboard)
+async def _render_campaign_edit_menu(
+ bot: Bot,
+ chat_id: int,
+ message_id: int,
+ campaign,
+ language: str,
+):
+ texts = get_texts(language)
+ text = (
+ "✏️ Редактирование кампании\n\n"
+ f"{_format_campaign_summary(campaign, texts)}\n"
+ "Выберите, что изменить:"
+ )
+
+ await bot.edit_message_text(
+ text=text,
+ chat_id=chat_id,
+ message_id=message_id,
+ reply_markup=get_campaign_edit_keyboard(
+ campaign.id,
+ is_balance_bonus=campaign.is_balance_bonus,
+ language=language,
+ ),
+ parse_mode="HTML",
+ )
+
+
@admin_required
@error_handler
async def show_campaigns_menu(
@@ -300,6 +335,761 @@ async def show_campaign_detail(
await callback.answer()
+@admin_required
+@error_handler
+async def show_campaign_edit_menu(
+ callback: types.CallbackQuery,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ campaign_id = int(callback.data.split("_")[-1])
+ campaign = await get_campaign_by_id(db, campaign_id)
+
+ if not campaign:
+ await state.clear()
+ await callback.answer("❌ Кампания не найдена", show_alert=True)
+ return
+
+ await state.clear()
+
+ await _render_campaign_edit_menu(
+ callback.bot,
+ callback.message.chat.id,
+ callback.message.message_id,
+ campaign,
+ db_user.language,
+ )
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def start_edit_campaign_name(
+ callback: types.CallbackQuery,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ campaign_id = int(callback.data.split("_")[-1])
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await callback.answer("❌ Кампания не найдена", show_alert=True)
+ return
+
+ await state.clear()
+ await state.set_state(AdminStates.editing_campaign_name)
+ await state.update_data(
+ editing_campaign_id=campaign_id,
+ campaign_edit_message_id=callback.message.message_id,
+ )
+
+ await callback.message.edit_text(
+ (
+ "✏️ Изменение названия кампании\n\n"
+ f"Текущее название: {campaign.name}\n"
+ "Введите новое название (3-100 символов):"
+ ),
+ reply_markup=types.InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ types.InlineKeyboardButton(
+ text="❌ Отмена",
+ callback_data=f"admin_campaign_edit_{campaign_id}",
+ )
+ ]
+ ]
+ ),
+ )
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def process_edit_campaign_name(
+ message: types.Message,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ data = await state.get_data()
+ campaign_id = data.get("editing_campaign_id")
+ if not campaign_id:
+ await message.answer("❌ Сессия редактирования устарела. Попробуйте снова.")
+ await state.clear()
+ return
+
+ new_name = message.text.strip()
+ if len(new_name) < 3 or len(new_name) > 100:
+ await message.answer(
+ "❌ Название должно содержать от 3 до 100 символов. Попробуйте снова."
+ )
+ return
+
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await message.answer("❌ Кампания не найдена")
+ await state.clear()
+ return
+
+ await update_campaign(db, campaign, name=new_name)
+ await state.clear()
+
+ await message.answer("✅ Название обновлено.")
+
+ edit_message_id = data.get("campaign_edit_message_id")
+ if edit_message_id:
+ await _render_campaign_edit_menu(
+ message.bot,
+ message.chat.id,
+ edit_message_id,
+ campaign,
+ db_user.language,
+ )
+
+
+@admin_required
+@error_handler
+async def start_edit_campaign_start_parameter(
+ callback: types.CallbackQuery,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ campaign_id = int(callback.data.split("_")[-1])
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await callback.answer("❌ Кампания не найдена", show_alert=True)
+ return
+
+ await state.clear()
+ await state.set_state(AdminStates.editing_campaign_start)
+ await state.update_data(
+ editing_campaign_id=campaign_id,
+ campaign_edit_message_id=callback.message.message_id,
+ )
+
+ await callback.message.edit_text(
+ (
+ "🔗 Изменение стартового параметра\n\n"
+ f"Текущий параметр: {campaign.start_parameter}\n"
+ "Введите новый параметр (латинские буквы, цифры, - или _, 3-32 символа):"
+ ),
+ reply_markup=types.InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ types.InlineKeyboardButton(
+ text="❌ Отмена",
+ callback_data=f"admin_campaign_edit_{campaign_id}",
+ )
+ ]
+ ]
+ ),
+ )
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def process_edit_campaign_start_parameter(
+ message: types.Message,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ data = await state.get_data()
+ campaign_id = data.get("editing_campaign_id")
+ if not campaign_id:
+ await message.answer("❌ Сессия редактирования устарела. Попробуйте снова.")
+ await state.clear()
+ return
+
+ new_param = message.text.strip()
+ if not _CAMPAIGN_PARAM_REGEX.match(new_param):
+ await message.answer(
+ "❌ Разрешены только латинские буквы, цифры, символы - и _. Длина 3-32 символа."
+ )
+ return
+
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await message.answer("❌ Кампания не найдена")
+ await state.clear()
+ return
+
+ existing = await get_campaign_by_start_parameter(db, new_param)
+ if existing and existing.id != campaign_id:
+ await message.answer("❌ Такой параметр уже используется. Введите другой вариант.")
+ return
+
+ await update_campaign(db, campaign, start_parameter=new_param)
+ await state.clear()
+
+ await message.answer("✅ Стартовый параметр обновлен.")
+
+ edit_message_id = data.get("campaign_edit_message_id")
+ if edit_message_id:
+ await _render_campaign_edit_menu(
+ message.bot,
+ message.chat.id,
+ edit_message_id,
+ campaign,
+ db_user.language,
+ )
+
+
+@admin_required
+@error_handler
+async def start_edit_campaign_balance_bonus(
+ callback: types.CallbackQuery,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ campaign_id = int(callback.data.split("_")[-1])
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await callback.answer("❌ Кампания не найдена", show_alert=True)
+ return
+
+ if not campaign.is_balance_bonus:
+ await callback.answer("❌ У кампании другой тип бонуса", show_alert=True)
+ return
+
+ await state.clear()
+ await state.set_state(AdminStates.editing_campaign_balance)
+ await state.update_data(
+ editing_campaign_id=campaign_id,
+ campaign_edit_message_id=callback.message.message_id,
+ )
+
+ await callback.message.edit_text(
+ (
+ "💰 Изменение бонуса на баланс\n\n"
+ f"Текущий бонус: {get_texts(db_user.language).format_price(campaign.balance_bonus_kopeks)}\n"
+ "Введите новую сумму в рублях (например, 100 или 99.5):"
+ ),
+ reply_markup=types.InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ types.InlineKeyboardButton(
+ text="❌ Отмена",
+ callback_data=f"admin_campaign_edit_{campaign_id}",
+ )
+ ]
+ ]
+ ),
+ )
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def process_edit_campaign_balance_bonus(
+ message: types.Message,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ data = await state.get_data()
+ campaign_id = data.get("editing_campaign_id")
+ if not campaign_id:
+ await message.answer("❌ Сессия редактирования устарела. Попробуйте снова.")
+ await state.clear()
+ return
+
+ try:
+ amount_rubles = float(message.text.replace(",", "."))
+ except ValueError:
+ await message.answer("❌ Введите корректную сумму (например, 100 или 99.5)")
+ return
+
+ if amount_rubles <= 0:
+ await message.answer("❌ Сумма должна быть больше нуля")
+ return
+
+ amount_kopeks = int(round(amount_rubles * 100))
+
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await message.answer("❌ Кампания не найдена")
+ await state.clear()
+ return
+
+ if not campaign.is_balance_bonus:
+ await message.answer("❌ У кампании другой тип бонуса")
+ await state.clear()
+ return
+
+ await update_campaign(db, campaign, balance_bonus_kopeks=amount_kopeks)
+ await state.clear()
+
+ await message.answer("✅ Бонус обновлен.")
+
+ edit_message_id = data.get("campaign_edit_message_id")
+ if edit_message_id:
+ await _render_campaign_edit_menu(
+ message.bot,
+ message.chat.id,
+ edit_message_id,
+ campaign,
+ db_user.language,
+ )
+
+
+async def _ensure_subscription_campaign(message_or_callback, campaign) -> bool:
+ if campaign.is_balance_bonus:
+ if isinstance(message_or_callback, types.CallbackQuery):
+ await message_or_callback.answer(
+ "❌ Для этой кампании доступен только бонус на баланс",
+ show_alert=True,
+ )
+ else:
+ await message_or_callback.answer(
+ "❌ Для этой кампании нельзя изменить параметры подписки"
+ )
+ return False
+ return True
+
+
+@admin_required
+@error_handler
+async def start_edit_campaign_subscription_days(
+ callback: types.CallbackQuery,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ campaign_id = int(callback.data.split("_")[-1])
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await callback.answer("❌ Кампания не найдена", show_alert=True)
+ return
+
+ if not await _ensure_subscription_campaign(callback, campaign):
+ return
+
+ await state.clear()
+ await state.set_state(AdminStates.editing_campaign_subscription_days)
+ await state.update_data(
+ editing_campaign_id=campaign_id,
+ campaign_edit_message_id=callback.message.message_id,
+ )
+
+ await callback.message.edit_text(
+ (
+ "📅 Изменение длительности подписки\n\n"
+ f"Текущее значение: {campaign.subscription_duration_days or 0} д.\n"
+ "Введите новое количество дней (1-730):"
+ ),
+ reply_markup=types.InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ types.InlineKeyboardButton(
+ text="❌ Отмена",
+ callback_data=f"admin_campaign_edit_{campaign_id}",
+ )
+ ]
+ ]
+ ),
+ )
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def process_edit_campaign_subscription_days(
+ message: types.Message,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ data = await state.get_data()
+ campaign_id = data.get("editing_campaign_id")
+ if not campaign_id:
+ await message.answer("❌ Сессия редактирования устарела. Попробуйте снова.")
+ await state.clear()
+ return
+
+ try:
+ days = int(message.text.strip())
+ except ValueError:
+ await message.answer("❌ Введите число дней (1-730)")
+ return
+
+ if days <= 0 or days > 730:
+ await message.answer("❌ Длительность должна быть от 1 до 730 дней")
+ return
+
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await message.answer("❌ Кампания не найдена")
+ await state.clear()
+ return
+
+ if not await _ensure_subscription_campaign(message, campaign):
+ await state.clear()
+ return
+
+ await update_campaign(db, campaign, subscription_duration_days=days)
+ await state.clear()
+
+ await message.answer("✅ Длительность подписки обновлена.")
+
+ edit_message_id = data.get("campaign_edit_message_id")
+ if edit_message_id:
+ await _render_campaign_edit_menu(
+ message.bot,
+ message.chat.id,
+ edit_message_id,
+ campaign,
+ db_user.language,
+ )
+
+
+@admin_required
+@error_handler
+async def start_edit_campaign_subscription_traffic(
+ callback: types.CallbackQuery,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ campaign_id = int(callback.data.split("_")[-1])
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await callback.answer("❌ Кампания не найдена", show_alert=True)
+ return
+
+ if not await _ensure_subscription_campaign(callback, campaign):
+ return
+
+ await state.clear()
+ await state.set_state(AdminStates.editing_campaign_subscription_traffic)
+ await state.update_data(
+ editing_campaign_id=campaign_id,
+ campaign_edit_message_id=callback.message.message_id,
+ )
+
+ current_traffic = campaign.subscription_traffic_gb or 0
+ traffic_text = "безлимит" if current_traffic == 0 else f"{current_traffic} ГБ"
+
+ await callback.message.edit_text(
+ (
+ "🌐 Изменение лимита трафика\n\n"
+ f"Текущее значение: {traffic_text}\n"
+ "Введите новый лимит в ГБ (0 = безлимит, максимум 10000):"
+ ),
+ reply_markup=types.InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ types.InlineKeyboardButton(
+ text="❌ Отмена",
+ callback_data=f"admin_campaign_edit_{campaign_id}",
+ )
+ ]
+ ]
+ ),
+ )
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def process_edit_campaign_subscription_traffic(
+ message: types.Message,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ data = await state.get_data()
+ campaign_id = data.get("editing_campaign_id")
+ if not campaign_id:
+ await message.answer("❌ Сессия редактирования устарела. Попробуйте снова.")
+ await state.clear()
+ return
+
+ try:
+ traffic = int(message.text.strip())
+ except ValueError:
+ await message.answer("❌ Введите целое число (0 или больше)")
+ return
+
+ if traffic < 0 or traffic > 10000:
+ await message.answer("❌ Лимит трафика должен быть от 0 до 10000 ГБ")
+ return
+
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await message.answer("❌ Кампания не найдена")
+ await state.clear()
+ return
+
+ if not await _ensure_subscription_campaign(message, campaign):
+ await state.clear()
+ return
+
+ await update_campaign(db, campaign, subscription_traffic_gb=traffic)
+ await state.clear()
+
+ await message.answer("✅ Лимит трафика обновлен.")
+
+ edit_message_id = data.get("campaign_edit_message_id")
+ if edit_message_id:
+ await _render_campaign_edit_menu(
+ message.bot,
+ message.chat.id,
+ edit_message_id,
+ campaign,
+ db_user.language,
+ )
+
+
+@admin_required
+@error_handler
+async def start_edit_campaign_subscription_devices(
+ callback: types.CallbackQuery,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ campaign_id = int(callback.data.split("_")[-1])
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await callback.answer("❌ Кампания не найдена", show_alert=True)
+ return
+
+ if not await _ensure_subscription_campaign(callback, campaign):
+ return
+
+ await state.clear()
+ await state.set_state(AdminStates.editing_campaign_subscription_devices)
+ await state.update_data(
+ editing_campaign_id=campaign_id,
+ campaign_edit_message_id=callback.message.message_id,
+ )
+
+ current_devices = campaign.subscription_device_limit or settings.DEFAULT_DEVICE_LIMIT
+
+ await callback.message.edit_text(
+ (
+ "📱 Изменение лимита устройств\n\n"
+ f"Текущее значение: {current_devices}\n"
+ f"Введите новое количество (1-{settings.MAX_DEVICES_LIMIT}):"
+ ),
+ reply_markup=types.InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ types.InlineKeyboardButton(
+ text="❌ Отмена",
+ callback_data=f"admin_campaign_edit_{campaign_id}",
+ )
+ ]
+ ]
+ ),
+ )
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def process_edit_campaign_subscription_devices(
+ message: types.Message,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ data = await state.get_data()
+ campaign_id = data.get("editing_campaign_id")
+ if not campaign_id:
+ await message.answer("❌ Сессия редактирования устарела. Попробуйте снова.")
+ await state.clear()
+ return
+
+ try:
+ devices = int(message.text.strip())
+ except ValueError:
+ await message.answer("❌ Введите целое число устройств")
+ return
+
+ if devices < 1 or devices > settings.MAX_DEVICES_LIMIT:
+ await message.answer(
+ f"❌ Количество устройств должно быть от 1 до {settings.MAX_DEVICES_LIMIT}"
+ )
+ return
+
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await message.answer("❌ Кампания не найдена")
+ await state.clear()
+ return
+
+ if not await _ensure_subscription_campaign(message, campaign):
+ await state.clear()
+ return
+
+ await update_campaign(db, campaign, subscription_device_limit=devices)
+ await state.clear()
+
+ await message.answer("✅ Лимит устройств обновлен.")
+
+ edit_message_id = data.get("campaign_edit_message_id")
+ if edit_message_id:
+ await _render_campaign_edit_menu(
+ message.bot,
+ message.chat.id,
+ edit_message_id,
+ campaign,
+ db_user.language,
+ )
+
+
+@admin_required
+@error_handler
+async def start_edit_campaign_subscription_servers(
+ callback: types.CallbackQuery,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ campaign_id = int(callback.data.split("_")[-1])
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await callback.answer("❌ Кампания не найдена", show_alert=True)
+ return
+
+ if not await _ensure_subscription_campaign(callback, campaign):
+ return
+
+ servers, _ = await get_all_server_squads(db, available_only=False)
+ if not servers:
+ await callback.answer(
+ "❌ Не найдены доступные серверы. Добавьте серверы перед изменением.",
+ show_alert=True,
+ )
+ return
+
+ selected = list(campaign.subscription_squads or [])
+
+ await state.clear()
+ await state.set_state(AdminStates.editing_campaign_subscription_servers)
+ await state.update_data(
+ editing_campaign_id=campaign_id,
+ campaign_edit_message_id=callback.message.message_id,
+ campaign_subscription_squads=selected,
+ )
+
+ keyboard = _build_campaign_servers_keyboard(
+ servers,
+ selected,
+ toggle_prefix=f"campaign_edit_toggle_{campaign_id}_",
+ save_callback=f"campaign_edit_servers_save_{campaign_id}",
+ back_callback=f"admin_campaign_edit_{campaign_id}",
+ )
+
+ await callback.message.edit_text(
+ (
+ "🌍 Редактирование доступных серверов\n\n"
+ "Нажмите на сервер, чтобы добавить или убрать его из кампании.\n"
+ "После выбора нажмите \"✅ Сохранить\"."
+ ),
+ reply_markup=keyboard,
+ )
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def toggle_edit_campaign_server(
+ callback: types.CallbackQuery,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ parts = callback.data.split("_")
+ try:
+ server_id = int(parts[-1])
+ except (ValueError, IndexError):
+ await callback.answer("❌ Не удалось определить сервер", show_alert=True)
+ return
+
+ data = await state.get_data()
+ campaign_id = data.get("editing_campaign_id")
+ if not campaign_id:
+ await callback.answer("❌ Сессия редактирования устарела", show_alert=True)
+ await state.clear()
+ return
+
+ server = await get_server_squad_by_id(db, server_id)
+ if not server:
+ await callback.answer("❌ Сервер не найден", show_alert=True)
+ return
+
+ selected = list(data.get("campaign_subscription_squads", []))
+
+ if server.squad_uuid in selected:
+ selected.remove(server.squad_uuid)
+ else:
+ selected.append(server.squad_uuid)
+
+ await state.update_data(campaign_subscription_squads=selected)
+
+ servers, _ = await get_all_server_squads(db, available_only=False)
+ keyboard = _build_campaign_servers_keyboard(
+ servers,
+ selected,
+ toggle_prefix=f"campaign_edit_toggle_{campaign_id}_",
+ save_callback=f"campaign_edit_servers_save_{campaign_id}",
+ back_callback=f"admin_campaign_edit_{campaign_id}",
+ )
+
+ await callback.message.edit_reply_markup(reply_markup=keyboard)
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def save_edit_campaign_subscription_servers(
+ callback: types.CallbackQuery,
+ db_user: User,
+ state: FSMContext,
+ db: AsyncSession,
+):
+ data = await state.get_data()
+ campaign_id = data.get("editing_campaign_id")
+ if not campaign_id:
+ await callback.answer("❌ Сессия редактирования устарела", show_alert=True)
+ await state.clear()
+ return
+
+ selected = list(data.get("campaign_subscription_squads", []))
+ if not selected:
+ await callback.answer("❗ Выберите хотя бы один сервер", show_alert=True)
+ return
+
+ campaign = await get_campaign_by_id(db, campaign_id)
+ if not campaign:
+ await state.clear()
+ await callback.answer("❌ Кампания не найдена", show_alert=True)
+ return
+
+ if not await _ensure_subscription_campaign(callback, campaign):
+ await state.clear()
+ return
+
+ await update_campaign(db, campaign, subscription_squads=selected)
+ await state.clear()
+
+ await _render_campaign_edit_menu(
+ callback.bot,
+ callback.message.chat.id,
+ callback.message.message_id,
+ campaign,
+ db_user.language,
+ )
+ await callback.answer("✅ Сохранено")
+
+
@admin_required
@error_handler
async def toggle_campaign_status(
@@ -386,8 +1176,8 @@ async def confirm_delete_campaign(
await callback.message.edit_text(
text,
reply_markup=get_confirmation_keyboard(
- confirm_callback=f"admin_campaign_delete_confirm_{campaign_id}",
- cancel_callback=f"admin_campaign_manage_{campaign_id}",
+ confirm_action=f"admin_campaign_delete_confirm_{campaign_id}",
+ cancel_action=f"admin_campaign_manage_{campaign_id}",
),
)
await callback.answer()
@@ -765,6 +1555,43 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(
show_campaign_detail, F.data.startswith("admin_campaign_manage_")
)
+ dp.callback_query.register(
+ start_edit_campaign_name, F.data.startswith("admin_campaign_edit_name_")
+ )
+ dp.callback_query.register(
+ start_edit_campaign_start_parameter,
+ F.data.startswith("admin_campaign_edit_start_"),
+ )
+ dp.callback_query.register(
+ start_edit_campaign_balance_bonus,
+ F.data.startswith("admin_campaign_edit_balance_"),
+ )
+ dp.callback_query.register(
+ start_edit_campaign_subscription_days,
+ F.data.startswith("admin_campaign_edit_sub_days_"),
+ )
+ dp.callback_query.register(
+ start_edit_campaign_subscription_traffic,
+ F.data.startswith("admin_campaign_edit_sub_traffic_"),
+ )
+ dp.callback_query.register(
+ start_edit_campaign_subscription_devices,
+ F.data.startswith("admin_campaign_edit_sub_devices_"),
+ )
+ dp.callback_query.register(
+ start_edit_campaign_subscription_servers,
+ F.data.startswith("admin_campaign_edit_sub_servers_"),
+ )
+ dp.callback_query.register(
+ save_edit_campaign_subscription_servers,
+ F.data.startswith("campaign_edit_servers_save_"),
+ )
+ dp.callback_query.register(
+ toggle_edit_campaign_server, F.data.startswith("campaign_edit_toggle_")
+ )
+ dp.callback_query.register(
+ show_campaign_edit_menu, F.data.startswith("admin_campaign_edit_")
+ )
dp.callback_query.register(
delete_campaign_confirmed, F.data.startswith("admin_campaign_delete_confirm_")
)
@@ -803,3 +1630,26 @@ def register_handlers(dp: Dispatcher):
process_campaign_subscription_devices,
AdminStates.creating_campaign_subscription_devices,
)
+ dp.message.register(
+ process_edit_campaign_name, AdminStates.editing_campaign_name
+ )
+ dp.message.register(
+ process_edit_campaign_start_parameter,
+ AdminStates.editing_campaign_start,
+ )
+ dp.message.register(
+ process_edit_campaign_balance_bonus,
+ AdminStates.editing_campaign_balance,
+ )
+ dp.message.register(
+ process_edit_campaign_subscription_days,
+ AdminStates.editing_campaign_subscription_days,
+ )
+ dp.message.register(
+ process_edit_campaign_subscription_traffic,
+ AdminStates.editing_campaign_subscription_traffic,
+ )
+ dp.message.register(
+ process_edit_campaign_subscription_devices,
+ AdminStates.editing_campaign_subscription_devices,
+ )
diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py
index d49daf82..3ecee4a5 100644
--- a/app/keyboards/admin.py
+++ b/app/keyboards/admin.py
@@ -167,21 +167,109 @@ def get_admin_campaigns_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
])
-def get_campaign_management_keyboard(campaign_id: int, is_active: bool, language: str = "ru") -> InlineKeyboardMarkup:
+def get_campaign_management_keyboard(
+ campaign_id: int, is_active: bool, language: str = "ru"
+) -> InlineKeyboardMarkup:
status_text = "🔴 Выключить" if is_active else "🟢 Включить"
- return InlineKeyboardMarkup(inline_keyboard=[
- [
- InlineKeyboardButton(text="📊 Статистика", callback_data=f"admin_campaign_stats_{campaign_id}"),
- InlineKeyboardButton(text=status_text, callback_data=f"admin_campaign_toggle_{campaign_id}")
- ],
- [
- InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"admin_campaign_delete_{campaign_id}")
- ],
- [
- InlineKeyboardButton(text="⬅️ К списку", callback_data="admin_campaigns_list")
+ return InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ InlineKeyboardButton(
+ text="📊 Статистика",
+ callback_data=f"admin_campaign_stats_{campaign_id}",
+ ),
+ InlineKeyboardButton(
+ text=status_text,
+ callback_data=f"admin_campaign_toggle_{campaign_id}",
+ ),
+ ],
+ [
+ InlineKeyboardButton(
+ text="✏️ Редактировать",
+ callback_data=f"admin_campaign_edit_{campaign_id}",
+ )
+ ],
+ [
+ InlineKeyboardButton(
+ text="🗑️ Удалить",
+ callback_data=f"admin_campaign_delete_{campaign_id}",
+ )
+ ],
+ [
+ InlineKeyboardButton(
+ text="⬅️ К списку", callback_data="admin_campaigns_list"
+ )
+ ],
]
- ])
+ )
+
+
+def get_campaign_edit_keyboard(
+ campaign_id: int,
+ *,
+ is_balance_bonus: bool,
+ language: str = "ru",
+) -> InlineKeyboardMarkup:
+ texts = get_texts(language)
+
+ keyboard: List[List[InlineKeyboardButton]] = [
+ [
+ InlineKeyboardButton(
+ text="✏️ Название",
+ callback_data=f"admin_campaign_edit_name_{campaign_id}",
+ ),
+ InlineKeyboardButton(
+ text="🔗 Параметр",
+ callback_data=f"admin_campaign_edit_start_{campaign_id}",
+ ),
+ ]
+ ]
+
+ if is_balance_bonus:
+ keyboard.append(
+ [
+ InlineKeyboardButton(
+ text="💰 Бонус на баланс",
+ callback_data=f"admin_campaign_edit_balance_{campaign_id}",
+ )
+ ]
+ )
+ else:
+ keyboard.extend(
+ [
+ [
+ InlineKeyboardButton(
+ text="📅 Длительность",
+ callback_data=f"admin_campaign_edit_sub_days_{campaign_id}",
+ ),
+ InlineKeyboardButton(
+ text="🌐 Трафик",
+ callback_data=f"admin_campaign_edit_sub_traffic_{campaign_id}",
+ ),
+ ],
+ [
+ InlineKeyboardButton(
+ text="📱 Устройства",
+ callback_data=f"admin_campaign_edit_sub_devices_{campaign_id}",
+ ),
+ InlineKeyboardButton(
+ text="🌍 Серверы",
+ callback_data=f"admin_campaign_edit_sub_servers_{campaign_id}",
+ ),
+ ],
+ ]
+ )
+
+ keyboard.append(
+ [
+ InlineKeyboardButton(
+ text=texts.BACK, callback_data=f"admin_campaign_manage_{campaign_id}"
+ )
+ ]
+ )
+
+ return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_campaign_bonus_type_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
diff --git a/app/states.py b/app/states.py
index 35f461e0..ac1f76f7 100644
--- a/app/states.py
+++ b/app/states.py
@@ -50,6 +50,14 @@ class AdminStates(StatesGroup):
creating_campaign_subscription_traffic = State()
creating_campaign_subscription_devices = State()
creating_campaign_subscription_servers = State()
+
+ editing_campaign_name = State()
+ editing_campaign_start = State()
+ editing_campaign_balance = State()
+ editing_campaign_subscription_days = State()
+ editing_campaign_subscription_traffic = State()
+ editing_campaign_subscription_devices = State()
+ editing_campaign_subscription_servers = State()
waiting_for_broadcast_message = State()
waiting_for_broadcast_media = State()