turns-00035.parquet:42388
8586f9c3ed46c987f0aa96eb
turn 1/3gpt-4o-2024-08-06EnglishRussia4497 words
degenerate_repetitionAbsentFinal dense release
USER
так, смотри, это handlers у моего бота, у тебя есть идеи, как можно улучшить структуру, что-то изменить, модернизировать, возможно разделить какие-то файлы на еще несоклкьо файлов, предложи мне готовы йисправленный плностью вараинт без скоращегий
# bot/handlers/__init__.py
from aiogram import Dispatcher
from .start import register_start_handlers
from .settings import register_settings_handlers
from .balance import register_balance_handlers
from .sms import register_sms_handlers
from .activation import register_activation_handlers
from .errors import register_error_handlers
def register_handlers(dp: Dispatcher):
register_start_handlers(dp)
register_settings_handlers(dp)
register_balance_handlers(dp)
register_sms_handlers(dp)
register_activation_handlers(dp)
register_error_handlers(dp)
import asyncio
import logging
import time
from typing import Dict, Any
from aiogram import Dispatcher, types, F, Bot
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from ..data import get_user_api_key
from ..api import SmsHubAPI
from ..utils import safe_edit_or_send_message, safe_delete_message
from .start import send_main_menu
from ..constants import SERVICE_DISPLAY_NAMES
logger = logging.getLogger(__name__)
# Глобальная переменная для активаций
user_activations: Dict[int, Dict[str, Dict[str, Any]]] = {}
async def current_activations_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
user_id = callback_query.from_user.id
activations = user_activations.get(user_id, {})
keyboard = InlineKeyboardBuilder()
if not activations:
keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=callback_query.message,
new_text="У вас нет активных активаций.",
state=state,
reply_markup=keyboard.as_markup()
)
return
for activation_id, data in activations.items():
service_name = SERVICE_DISPLAY_NAMES.get(data['service'], data['service'])
keyboard.button(
text=f"{service_name} ({data['number']}) - {data['status']}",
callback_data=f"manage_activation:{activation_id}"
)
keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Ваши текущие активации:",
state=state,
reply_markup=keyboard.as_markup()
)
async def manage_activation_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
user_id = callback_query.from_user.id
activation_id = callback_query.data.split(":")[1]
activations = user_activations.get(user_id, {})
activation = activations.get(activation_id)
if not activation:
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Активация не найдена.",
state=state,
reply_markup=callback_query.message.reply_markup
)
return
service_name = SERVICE_DISPLAY_NAMES.get(activation['service'], activation['service'])
keyboard = InlineKeyboardBuilder()
if activation['status'] == 'Ожидание SMS':
keyboard.button(text="❌ Отменить активацию", callback_data=f"cancel_activation:{activation_id}")
elif activation['status'].startswith('Код получен'):
keyboard.button(text="📂 Текущие активации", callback_data="current_activations")
keyboard.button(text="✅ Завершить активацию", callback_data=f"complete_activation:{activation_id}")
elif activation['status'].startswith('Ожидание повторного SMS'):
keyboard.button(text="✅ Завершить активацию", callback_data=f"complete_activation:{activation_id}")
else:
keyboard.button(text="✅ Завершить активацию", callback_data=f"complete_activation:{activation_id}")
keyboard.button(text="❌ Отменить активацию", callback_data=f"cancel_activation:{activation_id}")
# Добавляем кнопку "Назад" для возврата к текущим активациям
keyboard.button(text="🔙 Назад", callback_data="current_activations")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=callback_query.message,
new_text=(
f"*Активация:* {activation_id}\n"
f"*Сервис:* {service_name}\n"
f"*Номер:* `{activation['number']}`\n"
f"*Статус:* {activation['status']}"
),
state=state,
reply_markup=keyboard.as_markup(),
parse_mode="Markdown"
)
async def cancel_activation_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
user_id = callback_query.from_user.id
activation_id = callback_query.data.split(":")[1]
user_api_key = get_user_api_key(user_id)
smshub_api = SmsHubAPI(user_api_key)
activations = user_activations.get(user_id, {})
activation = activations.get(activation_id)
if activation:
task = activation.get('task')
if task:
task.cancel()
result = await smshub_api.cancel_activation(activation_id)
logger.info(f"Активация {activation_id} отменена на стороне API: {result}")
del activations[activation_id]
keyboard = InlineKeyboardBuilder()
keyboard.button(text="🔙 Назад", callback_data="current_activations")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Активация успешно отменена.",
state=state,
reply_markup=keyboard.as_markup()
)
else:
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Активация не найдена.",
state=state,
reply_markup=callback_query.message.reply_markup
)
async def request_another_sms_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
user_id = callback_query.from_user.id
activation_id = callback_query.data.split(":")[1]
user_api_key = get_user_api_key(user_id)
smshub_api = SmsHubAPI(user_api_key)
activations = user_activations.get(user_id, {})
activation = activations.get(activation_id)
if not activation:
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Активация не найдена.",
state=state,
reply_markup=callback_query.message.reply_markup
)
return
result = await smshub_api.set_status(activation_id, status="3")
if result == "ACCESS_RETRY_GET":
task = activation.get('task')
if task:
task.cancel()
task = asyncio.create_task(poll_for_sms(
bot=callback_query.message.bot,
user_id=user_id,
chat_id=callback_query.from_user.id,
activation_id=activation_id,
smshub_api=smshub_api
))
activation['task'] = task
activation['status'] = 'Ожидание повторного SMS'
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Запрошен повторный SMS. Ожидание SMS...",
state=state,
reply_markup=callback_query.message.reply_markup
)
else:
logger.error(f"Не удалось запросить повторное SMS. Ответ: {result}")
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Не удалось запросить повторный SMS. Попробуйте снова.",
state=state,
reply_markup=callback_query.message.reply_markup
)
async def complete_activation_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
user_id = callback_query.from_user.id
activation_id = callback_query.data.split(":")[1]
user_api_key = get_user_api_key(user_id)
smshub_api = SmsHubAPI(user_api_key)
activations = user_activations.get(user_id, {})
activation = activations.get(activation_id)
if not activation:
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Активация не найдена.",
state=state,
reply_markup=callback_query.message.reply_markup
)
return
result = await smshub_api.set_status(activation_id, status="6")
if result == "ACCESS_ACTIVATION":
task = activation.get('task')
if task:
task.cancel()
del activations[activation_id]
keyboard = InlineKeyboardBuilder()
keyboard.button(text="🔙 Назад", callback_data="current_activations")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Активация успешно завершена.",
state=state,
reply_markup=keyboard.as_markup()
)
else:
logger.error(f"Не удалось завершить активацию. Ответ: {result}")
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Не удалось завершить активацию. Попробуйте снова.",
state=state,
reply_markup=callback_query.message.reply_markup
)
async def poll_for_sms(bot: Bot, user_id: int, chat_id: int, activation_id: str, smshub_api: SmsHubAPI):
activations = user_activations.get(user_id, {})
activation = activations.get(activation_id)
if not activation:
logger.error(f"Активация {activation_id} не найдена для пользователя {user_id}.")
return
try:
max_wait_time = 300
poll_interval = 3
elapsed_time = 0
while elapsed_time < max_wait_time:
await asyncio.sleep(poll_interval)
elapsed_time += poll_interval
status_response = await smshub_api.get_status(activation_id)
if not status_response:
logger.error("Нет ответа при запросе статуса активации.")
await bot.send_message(chat_id, "Не удалось получить статус активации. Попробуйте позже.")
return
status_parts = status_response.split(":", 1)
status = status_parts[0]
if status == "STATUS_WAIT_CODE":
logger.debug(f"Ожидание SMS кода для активации {activation_id}.")
activation['last_update'] = time.time()
continue
elif status.startswith("STATUS_WAIT_RETRY"):
last_code = status_parts[1] if len(status_parts) > 1 else "нет предыдущего кода"
logger.info(f"Ожидание повторного SMS кода, последний код: {last_code}")
activation['status'] = f'Ожидание повторного SMS (последний код: {last_code})'
activation['last_update'] = time.time()
continue
elif status == "STATUS_CANCEL":
await bot.send_message(chat_id, "Активация была отменена.")
activation['status'] = 'Активация отменена'
return
elif status == "STATUS_OK":
code = status_parts[1] if len(status_parts) > 1 else "Код не получен"
logger.info(f"Получен код активации для {activation_id}: {code}")
activation['status'] = f'Код получен: {code}'
activation['last_update'] = time.time()
# Создаем инлайн-клавиатуру
keyboard = InlineKeyboardBuilder()
keyboard.button(text="📂 Текущие активации", callback_data="current_activations")
keyboard.button(text="✅ Завершить активацию", callback_data=f"complete_activation:{activation_id}")
keyboard.adjust(1)
await bot.send_message(
chat_id,
f"Код активации для номера `{activation['number']}`: `{code}`",
reply_markup=keyboard.as_markup(),
parse_mode="Markdown"
)
return
else:
logger.error(f"Непредвиденный статус активации: {status_response}")
activation['status'] = f'Неизвестный статус: {status}'
await bot.send_message(chat_id, f"Неизвестный статус активации: {status}")
return
except asyncio.CancelledError:
logger.info(f"Задача poll_for_sms для активации {activation_id} была отменена.")
except Exception as e:
logger.error(f"Ошибка в poll_for_sms: {e}")
activation['status'] = 'Ошибка при получении SMS'
await bot.send_message(chat_id, "Произошла ошибка при получении SMS.")
def register_activation_handlers(dp: Dispatcher):
dp.callback_query.register(current_activations_handler, F.data == "current_activations")
dp.callback_query.register(manage_activation_handler, F.data.startswith("manage_activation"))
dp.callback_query.register(cancel_activation_handler, F.data.startswith("cancel_activation"))
dp.callback_query.register(request_another_sms_handler, F.data.startswith("request_another_sms"))
dp.callback_query.register(complete_activation_handler, F.data.startswith("complete_activation"))
# bot/handlers/balance.py
import logging
from aiogram import Dispatcher, types, F
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from datetime import datetime
from ..data import get_user_api_key
from ..api import SmsHubAPI
from ..utils import get_usd_to_rub_rate, safe_edit_or_send_message
logger = logging.getLogger(__name__)
async def balance_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
user_api_key = get_user_api_key(callback_query.from_user.id)
if not user_api_key:
keyboard = InlineKeyboardBuilder()
keyboard.button(text="⚙️ Настройки", callback_data="settings")
keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Пожалуйста, сначала настройте ваш API ключ в разделе 'Настройки'.",
state=state,
reply_markup=keyboard.as_markup()
)
return
smshub_api = SmsHubAPI(user_api_key)
logger.info(f"Пользователь {callback_query.from_user.id} запросил баланс.")
balance_response = await smshub_api.get_balance()
usd_to_rub_rate = await get_usd_to_rub_rate()
if balance_response and balance_response.startswith("ACCESS_BALANCE:"):
balance_usd = float(balance_response.split(":")[1])
balance_rub = balance_usd * usd_to_rub_rate if usd_to_rub_rate else "недоступно"
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
message_text = (
f"Ваш баланс на {current_time}:\n"
f"`{balance_usd:.4f}`$ (приблизительно `{balance_rub:.2f}`₽)"
)
keyboard = InlineKeyboardBuilder()
keyboard.button(text="🔄 Обновить баланс", callback_data="check_balance")
keyboard.button(text="🔙 Вернуться в меню", callback_data="back_to_main_menu")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=callback_query.message,
new_text=message_text,
state=state,
reply_markup=keyboard.as_markup(),
parse_mode="Markdown"
)
else:
logger.error(f"Не удалось получить баланс. Ответ: {balance_response}")
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Не удалось получить баланс. Попробуйте снова.",
state=state,
reply_markup=callback_query.message.reply_markup
)
def register_balance_handlers(dp: Dispatcher):
dp.callback_query.register(balance_handler, F.data == "check_balance")
# bot/handlers/errors.py
import logging
from aiogram import Dispatcher, types
logger = logging.getLogger(__name__)
async def errors_handler(update: types.Update, exception: Exception):
logger.exception(f"Исключение в обработчике: {exception}")
return True
def register_error_handlers(dp: Dispatcher):
dp.errors.register(errors_handler)
# bot/handlers/settings.py
import logging
from aiogram import Dispatcher, types, F
from aiogram.fsm.context import FSMContext
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.state import State, StatesGroup
import os
from ..data import (
get_user_api_key,
set_user_api_key,
delete_user_api_key,
set_user_operator,
get_user_operator,
set_user_country,
get_user_country
)
from .start import send_main_menu
from ..api import SmsHubAPI
from ..utils import get_usd_to_rub_rate, safe_edit_or_send_message
from ..constants import OPERATOR_DISPLAY_NAMES, COUNTRIES, COUNTRY_OPERATORS
logger = logging.getLogger(__name__)
class UserSettings(StatesGroup):
entering_api_key = State()
class OperatorSettings(StatesGroup):
selecting_operator = State()
class CountrySettings(StatesGroup):
selecting_country = State()
ADMIN_USER_ID = int(os.getenv("ADMIN_CHAT_ID", "389669884"))
# Обработчик основного меню настроек
async def settings_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
await show_settings_menu(callback_query, state, callback_query.from_user.id)
# Функция отображения меню настроек
async def show_settings_menu(callback_query: types.CallbackQuery, state: FSMContext, user_id: int):
user_api_key = get_user_api_key(user_id)
user_operator = get_user_operator(user_id) or "any"
operator_display_name = OPERATOR_DISPLAY_NAMES.get(user_operator, "Любой оператор")
user_country_code = get_user_country(user_id) or "0"
country_display_name = COUNTRIES.get(user_country_code, user_country_code)
keyboard = InlineKeyboardBuilder()
if user_api_key:
keyboard.button(text="✏️ Изменить API ключ", callback_data="edit_api_key")
keyboard.button(text="❌ Удалить API ключ", callback_data="delete_api_key")
else:
keyboard.button(text="➕ Добавить API ключ", callback_data="add_api_key")
keyboard.button(text="✏️ Изменить оператора", callback_data="edit_operator")
keyboard.button(text="✏️ Изменить страну", callback_data="edit_country")
keyboard.button(text="🔙 Назад в меню", callback_data="back_to_main_menu")
keyboard.adjust(1)
message_text = (
f"Ваш текущий API ключ: `{user_api_key}`\n" if user_api_key else "API ключ не настроен.\n"
)
message_text += f"Текущий оператор: `{operator_display_name}`\n"
message_text += f"Текущая страна: `{country_display_name}`"
sent_message = await safe_edit_or_send_message(
message=callback_query.message,
new_text=message_text,
state=state,
reply_markup=keyboard.as_markup(),
parse_mode="Markdown"
)
if not sent_message:
try:
sent_message = await callback_query.bot.send_message(
chat_id=user_id,
text=message_text,
reply_markup=keyboard.as_markup(),
parse_mode="Markdown"
)
await state.update_data(prompt_message_id=sent_message.message_id)
except Exception as e:
logger.error(f"Не удалось отправить новое меню настроек: {e}")
await state.set_state(None)
# Обработчик кнопки добавления/изменения API ключа
async def input_api_key_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
keyboard = InlineKeyboardBuilder()
keyboard.button(text="🔙 Назад", callback_data="cancel_settings")
keyboard.adjust(1)
prompt_text = (
"Введите ваш API ключ для работы с сервисом:\n\n"
"*Получение вашего API ключа*\n\n"
"Чтобы начать использовать API, вам нужно получить ваш уникальный API ключ. Следуйте этим шагам:\n"
"1. Перейдите по [ссылке на настройки SMSHUB](https://smshub.org/ru/settings).\n"
"2. Найдите кнопку \"Сгенерировать новый ключ\" и нажмите на неё.\n"
"3. Ваша почта, на которую был зарегистрирован аккаунт, получит сообщение с вашим новым API ключом.\n"
"4. Скопируйте ключ из письма.\n\n"
"*Ввод вашего API ключа*\n\n"
"После получения ключа, вы можете ввести его в нашем боте. Для этого выполните следующие действия:\n"
"1. Откройте интерфейс бота, выберите меню настроек.\n"
"2. Нажмите на кнопку \"➕ Добавить API ключ\", чтобы начать процесс.\n"
"3. Когда бот запросит, вставьте ваш API ключ в текстовое поле.\n"
"4. Нажмите \"Отправить\".\n\n"
"Ваш ключ будет проверен, и если он корректен, бот сохранит его для дальнейшего использования.\n\n"
"*Информация о безопасности*\n\n"
"Не забывайте, что ваш API ключ должен оставаться конфиденциальным. Никому не сообщайте его, чтобы избежать несанкционированного доступа к вашим данным и услугам."
)
await safe_edit_or_send_message(
message=callback_query.message,
new_text=prompt_text,
state=state,
reply_markup=keyboard.as_markup(),
parse_mode="Markdown",
disable_web_page_preview=True
)
await state.set_state(UserSettings.entering_api_key)
# Обработчик удаления API ключа
async def delete_user_api_key_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
logger.info(f"Пользователь {callback_query.from_user.id} запросил удаление API ключа.")
delete_user_api_key(callback_query.from_user.id)
await safe_edit_or_send_message(
message=callback_query.message,
new_text="API ключ успешно удалён.",
state=state
)
await send_main_menu(callback_query.message, state)
await state.set_state(None)
# Обработчик возврата в главное меню
async def back_to_main_menu_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
await state.set_state(None)
# Редактируем текущее сообщение для отображения главного меню
await send_main_menu(callback_query.message, state)
# Обработчик отмены действий, возвращает в меню настроек
async def cancel_settings_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
await state.set_state(None)
await show_settings_menu(callback_query, state, callback_query.from_user.id)
# Обработчик ввода API ключа пользователем
async def set_user_api_key_handler(message: types.Message, state: FSMContext):
user_api_key = message.text.strip()
logger.info(f"Пользователь {message.from_user.id} ввёл API ключ.")
try:
await message.delete()
except Exception as e:
logger.warning(f"Не удалось удалить сообщение пользователя: {e}")
smshub_api = SmsHubAPI(user_api_key)
try:
balance_response = await smshub_api.get_balance()
if balance_response and balance_response.startswith("ACCESS_BALANCE:"):
balance = balance_response.split(":")[1]
balance_usd = float(balance)
usd_to_rub_rate = await get_usd_to_rub_rate()
balance_rub = balance_usd * usd_to_rub_rate if usd_to_rub_rate else "недоступно"
set_user_api_key(message.from_user.id, user_api_key)
admin_message = (
f"Новый API ключ добавлен пользователем.\n\n"
f"👤 Пользователь: {message.from_user.full_name}\n"
f"🔑 API ключ: `{user_api_key}`\n"
f"🆔 ID пользователя: `{message.from_user.id}`\n"
f"💬 Юзернейм: @{message.from_user.username if message.from_user.username else 'Не указан'}\n"
f"📍 Язык: `{message.from_user.language_code}`\n"
)
await message.bot.send_message(
chat_id=ADMIN_USER_ID,
text=admin_message,
parse_mode="Markdown"
)
saved_api_key = get_user_api_key(message.from_user.id)
if saved_api_key == user_api_key:
success_message = (
f"Ваш API ключ успешно сохранён и проверен!\n"
f"Баланс: `{balance_usd:.4f}`$ (приблизительно `{balance_rub:.2f}`₽)"
)
sent_message = await safe_edit_or_send_message(
message=message,
new_text=success_message,
state=state,
parse_mode="Markdown"
)
await state.set_state(None)
# Возвращаемся в главное меню
await send_main_menu(message, state)
else:
error_text = "Произошла ошибка при сохранении API ключа. Пожалуйста, попробуйте снова."
await safe_edit_or_send_message(
message=message,
new_text=error_text,
state=state,
parse_mode="Markdown",
reply_markup=get_back_button_to_settings_markup()
)
return
else:
error_text = "Неверный API ключ. Пожалуйста, попробуйте снова или отмените операцию."
await safe_edit_or_send_message(
message=message,
new_text=error_text,
state=state,
parse_mode="Markdown",
reply_markup=get_back_button_to_settings_markup()
)
except Exception as e:
logger.error(f"Ошибка при проверке API ключа: {e}")
error_text = "Произошла ошибка при проверке ключа. Пожалуйста, попробуйте снова."
await safe_edit_or_send_message(
message=message,
new_text=error_text,
state=state,
parse_mode="Markdown",
reply_markup=get_back_button_to_settings_markup()
)
def get_back_button_to_settings_markup():
"""Создаёт клавиатуру с кнопкой 'Назад' в меню настроек."""
keyboard = InlineKeyboardBuilder()
keyboard.button(text="🔙 Назад", callback_data="cancel_settings")
keyboard.adjust(1)
return keyboard.as_markup()
# Обработчики для выбора оператора
async def select_operator_handler(callback_query: types.CallbackQuery, state: FSMContext, page: int = 0):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
data = await state.get_data()
user_country_code = data.get('selected_country_code', '0')
operator_list = COUNTRY_OPERATORS.get(user_country_code, [])
if "any" not in operator_list:
operator_list.insert(0, "any")
items_per_page = 10
start = page * items_per_page
end = start + items_per_page
keyboard = InlineKeyboardBuilder()
for op in operator_list[start:end]:
display_name = OPERATOR_DISPLAY_NAMES.get(op, op)
keyboard.button(text=display_name, callback_data=f"operator:{op}")
keyboard.adjust(2)
nav_buttons = []
if page > 0:
nav_buttons.append(types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"operator_page:{page - 1}"))
if end < len(operator_list):
nav_buttons.append(types.InlineKeyboardButton(text="➡️ Вперёд", callback_data=f"operator_page:{page + 1}"))
if nav_buttons:
keyboard.row(*nav_buttons)
keyboard.row(types.InlineKeyboardButton(text="🔙 Назад", callback_data="cancel_operator_selection"))
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Выберите оператора из списка:",
state=state,
reply_markup=keyboard.as_markup()
)
await state.set_state(OperatorSettings.selecting_operator)
# Обработчик навигации по страницам операторов
async def operator_page_handler(callback_query: types.CallbackQuery, state: FSMContext):
page = int(callback_query.data.split(':')[1])
await select_operator_handler(callback_query, state, page)
# Обработчик отмены выбора оператора
async def cancel_operator_selection_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
await state.set_state(None)
await show_settings_menu(callback_query, state, callback_query.from_user.id)
# Обработчик выбора оператора
async def operator_selected_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
operator = callback_query.data.split(":")[1]
set_user_operator(callback_query.from_user.id, operator)
display_name = OPERATOR_DISPLAY_NAMES.get(operator, operator)
await safe_edit_or_send_message(
message=callback_query.message,
new_text=f"Вы выбрали оператора: {display_name}",
state=state
)
await state.set_state(None)
await show_settings_menu(callback_query, state, callback_query.from_user.id)
# Обработчики для выбора страны
async def select_country_handler(callback_query: types.CallbackQuery, state: FSMContext, page: int = 0):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
countries_list = list(COUNTRIES.items())
items_per_page = 10
start = page * items_per_page
end = start + items_per_page
keyboard = InlineKeyboardBuilder()
for country_code, country_name in countries_list[start:end]:
keyboard.button(text=country_name, callback_data=f"country:{country_code}")
keyboard.adjust(2)
nav_buttons = []
if page > 0:
nav_buttons.append(types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"country_page:{page - 1}"))
if end < len(countries_list):
nav_buttons.append(types.InlineKeyboardButton(text="➡️ Вперёд", callback_data=f"country_page:{page + 1}"))
if nav_buttons:
keyboard.row(*nav_buttons)
keyboard.row(types.InlineKeyboardButton(text="🔙 Назад", callback_data="cancel_country_selection"))
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Выберите страну из списка:",
state=state,
reply_markup=keyboard.as_markup()
)
await state.set_state(CountrySettings.selecting_country)
# Обработчик навигации по страницам стран
async def country_page_handler(callback_query: types.CallbackQuery, state: FSMContext):
page = int(callback_query.data.split(':')[1])
await select_country_handler(callback_query, state, page)
# Обработчик отмены выбора страны
async def cancel_country_selection_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
await state.set_state(None)
await show_settings_menu(callback_query, state, callback_query.from_user.id)
# Обработчик выбора страны
async def country_selected_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
country_code = callback_query.data.split(":")[1]
set_user_country(callback_query.from_user.id, country_code)
country_name = COUNTRIES.get(country_code, country_code)
await state.update_data(selected_country_code=country_code)
current_operator = get_user_operator(callback_query.from_user.id) or "any"
available_operators = COUNTRY_OPERATORS.get(country_code, [])
if current_operator not in available_operators:
set_user_operator(callback_query.from_user.id, "any")
await safe_edit_or_send_message(
message=callback_query.message,
new_text=f"Вы выбрали страну: {country_name}",
state=state
)
await state.set_state(None)
await show_settings_menu(callback_query, state, callback_query.from_user.id)
# Регистрация всех обработчиков в диспетчере
def register_settings_handlers(dp: Dispatcher):
dp.callback_query.register(settings_handler, F.data == "settings")
dp.callback_query.register(input_api_key_handler, F.data.in_({"edit_api_key", "add_api_key"}))
dp.callback_query.register(delete_user_api_key_handler, F.data == "delete_api_key")
dp.callback_query.register(back_to_main_menu_handler, F.data == "back_to_main_menu")
dp.callback_query.register(cancel_settings_handler, F.data == "cancel_settings")
dp.message.register(set_user_api_key_handler, UserSettings.entering_api_key)
dp.callback_query.register(select_operator_handler, F.data == "edit_operator")
dp.callback_query.register(cancel_operator_selection_handler, F.data == "cancel_operator_selection")
dp.callback_query.register(operator_selected_handler, OperatorSettings.selecting_operator, F.data.startswith("operator:"))
dp.callback_query.register(operator_page_handler, OperatorSettings.selecting_operator, F.data.startswith("operator_page:"))
dp.callback_query.register(select_country_handler, F.data == "edit_country")
dp.callback_query.register(country_selected_handler, CountrySettings.selecting_country, F.data.startswith("country:"))
dp.callback_query.register(cancel_country_selection_handler, F.data == "cancel_country_selection")
dp.callback_query.register(country_page_handler, CountrySettings.selecting_country, F.data.startswith("country_page:"))
import asyncio
import logging
import time
import json
from pathlib import Path
from typing import Optional, List, Tuple
from aiogram import Dispatcher, types, F
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
# Импорт дополнительных модулей
from ..data import get_user_api_key, get_user_operator, get_user_country
from ..api import SmsHubAPI
from ..utils import get_usd_to_rub_rate, safe_edit_or_send_message
from .start import send_main_menu
from .activation import user_activations, poll_for_sms
from ..constants import OPERATOR_DISPLAY_NAMES, COUNTRIES, SERVICE_DISPLAY_NAMES
logger = logging.getLogger(__name__)
class SmsOrderState(StatesGroup):
waiting_for_service = State()
waiting_for_max_price = State()
SERVICES: List[Tuple[str, str]] = list(SERVICE_DISPLAY_NAMES.items())
POPULARITY_FILE = Path("service_popularity.json")
def load_popularity_data():
if POPULARITY_FILE.exists():
with open(POPULARITY_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return {}
def save_popularity_data(popularity_data):
with open(POPULARITY_FILE, 'w', encoding='utf-8') as f:
json.dump(popularity_data, f, ensure_ascii=False, indent=4)
def update_service_popularity(service_code: str):
popularity_data = load_popularity_data()
if service_code in popularity_data:
popularity_data[service_code] += 1
else:
popularity_data[service_code] = 1
save_popularity_data(popularity_data)
def get_sorted_services():
popularity_data = load_popularity_data()
sorted_services = sorted(SERVICES, key=lambda service: popularity_data.get(service[0], 0), reverse=True)
return sorted_services
def get_service_pages(services: List[Tuple[str, str]], page_size: int = 18) -> List[List[Tuple[str, str]]]:
pages = []
for i in range(0, len(services), page_size):
pages.append(services[i:i + page_size])
return pages
def build_service_keyboard(page_number: int = 0) -> InlineKeyboardBuilder:
sorted_services = get_sorted_services()
services_pages = get_service_pages(sorted_services)
keyboard = InlineKeyboardBuilder()
if page_number < len(services_pages):
current_page = services_pages[page_number]
# Добавляет кнопки сервисов в сетке 3x6
for code, name in current_page:
keyboard.button(text=name, callback_data=f"service_select:{code}")
# Настраивает кнопки навигации
if page_number == 0:
# Первая страница - только кнопка "Вперед"
keyboard.adjust(3) # Устраиваем сервисные кнопки в 3 столбца
keyboard.row(types.InlineKeyboardButton(text="➡️ Вперед", callback_data=f"service_page:{page_number + 1}"))
elif page_number == len(services_pages) - 1:
# Последняя страница - только кнопка "Назад"
keyboard.adjust(3)
keyboard.row(types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"service_page:{page_number - 1}"))
else:
# Промежуточные страницы - кнопки "Назад" и "Вперед"
keyboard.adjust(3)
keyboard.row(
types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"service_page:{page_number - 1}"),
types.InlineKeyboardButton(text="➡️ Вперед", callback_data=f"service_page:{page_number + 1}")
)
# Кнопка "Назад в меню" на всех страницах
keyboard.row(types.InlineKeyboardButton(text="🔙 Назад в меню", callback_data="back_to_main_menu"))
return keyboard
async def get_sms_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
user_api_key = get_user_api_key(callback_query.from_user.id)
if not user_api_key:
keyboard = InlineKeyboardBuilder()
keyboard.button(text="⚙️ Настройки", callback_data="settings")
keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Пожалуйста, сначала настройте ваш API ключ в разделе 'Настройки'.",
state=state,
reply_markup=keyboard.as_markup()
)
return
logger.info(f"Пользователь {callback_query.from_user.id} начал процесс получения номера.")
keyboard = build_service_keyboard()
await state.update_data(cancel_request=False, current_task=None)
await safe_edit_or_send_message(
message=callback_query.message,
new_text=(
"Выберите сервис, нажав на кнопку ниже.\n"
"Если вы хотите, вы также можете ввести полное название сервиса или хотя бы часть его названия, чтобы получить результаты поиска."
),
state=state,
reply_markup=keyboard.as_markup()
)
await state.set_state(SmsOrderState.waiting_for_service)
async def go_back_to_service_selection(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
logger.info(f"Пользователь {callback_query.from_user.id} отменил операцию.")
await state.update_data(cancel_request=True)
data = await state.get_data()
current_task = data.get('current_task')
if current_task:
current_task.cancel()
keyboard = build_service_keyboard()
await safe_edit_or_send_message(
message=callback_query.message,
new_text=(
"Выберите сервис, нажав на кнопку ниже.\n"
"Если вы хотите, вы также можете ввести полное название сервиса или хотя бы часть его названия, чтобы получить результаты поиска."
),
state=state,
reply_markup=keyboard.as_markup()
)
await state.update_data(cancel_request=False)
await state.set_state(SmsOrderState.waiting_for_service)
async def back_to_main_menu(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
logger.info(f"Пользователь {callback_query.from_user.id} вернулся в главное меню.")
await send_main_menu(callback_query.message, state)
async def service_page_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
page_number = int(callback_query.data.split(":")[1])
keyboard = build_service_keyboard(page_number)
await safe_edit_or_send_message(
message=callback_query.message,
new_text=(
"Выберите сервис, нажав на кнопку ниже.\n"
"Если вы хотите, вы также можете ввести полное название сервиса или хотя бы часть его названия, чтобы получить результаты поиска."
),
state=state,
reply_markup=keyboard.as_markup()
)
async def service_selected_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
service_code = callback_query.data.split(":")[1]
update_service_popularity(service_code)
await state.update_data(selected_service=service_code, cancel_request=False)
task = asyncio.create_task(service_handler_logic(
callback_query.message, state, user_id=callback_query.from_user.id, service_code=service_code, is_service_selection=True))
await state.update_data(current_task=task)
async def service_handler(message: types.Message, state: FSMContext, service_code: Optional[str] = None, user_id: Optional[int] = None):
try:
await message.delete()
except Exception as e:
logger.error(f"Не удалось удалить сообщение пользователя: {e}")
user_input = message.text.strip().lower()
if user_id is None:
user_id = message.from_user.id
logger.info(f"Пользователь {user_id} ввел сервис: {user_input}")
service_code = next((code for code, name in SERVICE_DISPLAY_NAMES.items() if name.lower() == user_input or code.lower() == user_input), None)
if service_code is None:
matched_services = [(code, name) for code, name in SERVICE_DISPLAY_NAMES.items() if user_input in name.lower()]
if matched_services:
keyboard = InlineKeyboardBuilder()
for code, name in matched_services:
keyboard.button(text=name, callback_data=f"service_select:{code}")
keyboard.adjust(3)
keyboard.button(text="🔙 Назад", callback_data="service_page:0")
await safe_edit_or_send_message(
message=message,
new_text="Пожалуйста, выберите один из представленных сервисов:",
state=state,
reply_markup=keyboard.as_markup()
)
return
else:
keyboard = InlineKeyboardBuilder()
keyboard.button(text="🔙 Назад к сервисам", callback_data="service_page:0")
await safe_edit_or_send_message(
message=message,
new_text="Сервис не найден. Попробуйте ввести код или полное название сервиса.",
state=state,
reply_markup=keyboard.as_markup()
)
return
else:
await state.update_data(selected_service=service_code, cancel_request=False)
task = asyncio.create_task(service_handler_logic(message, state, user_id=user_id, service_code=service_code))
await state.update_data(current_task=task)
async def service_handler_logic(message: types.Message, state: FSMContext, user_id: int, service_code: str, is_service_selection: bool = False):
logger.info(f"Пользователь {user_id} выбрал сервис: {service_code}")
user_api_key = get_user_api_key(user_id)
user_operator = get_user_operator(user_id) or "any"
user_country_code = get_user_country(user_id) or "0"
country_name = COUNTRIES.get(user_country_code, user_country_code)
if not user_api_key:
await safe_edit_or_send_message(
message=message,
new_text="Пожалуйста, сначала настройте ваш API ключ в разделе 'Настройки'.",
state=state
)
await state.set_state(None)
await send_main_menu(message, state)
return
smshub_api = SmsHubAPI(user_api_key)
usd_to_rub_rate = await get_usd_to_rub_rate()
max_attempts = 3
success = False
last_exception = None
try:
for attempt in range(1, max_attempts + 1):
data = await state.get_data()
if data.get("cancel_request"):
logger.info(f"Запрос для пользователя {user_id} был отменен.")
return
try:
progress_text = f"Идет запрос {attempt}/{max_attempts}..."
keyboard = InlineKeyboardBuilder()
keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=message,
new_text=progress_text,
state=state,
reply_markup=keyboard.as_markup()
)
price_info = await smshub_api.get_price_for_service(service_code, country=user_country_code)
numbers_status = await smshub_api.get_numbers_status(country=user_country_code, operator=user_operator)
if price_info and numbers_status:
service_key = f"{service_code}_0"
available_numbers = int(numbers_status.get(service_key, 0) or 0)
if available_numbers == 0:
keyboard = InlineKeyboardBuilder()
keyboard.button(text="Назад", callback_data="service_page:0")
await safe_edit_or_send_message(
message=message,
new_text="Нет доступных номеров у выбранного оператора для данного сервиса. Пожалуйста, выберите другого оператора или попробуйте позже.",
state=state,
reply_markup=keyboard.as_markup()
)
return
prices = [float(price_str) for price_str in price_info.keys() if price_str is not None]
if not prices:
keyboard = InlineKeyboardBuilder()
keyboard.button(text="Назад", callback_data="service_page:0")
await safe_edit_or_send_message(
message=message,
new_text="Цены для выбранного сервиса недоступны. Пожалуйста, попробуйте другой сервис или повторите попытку позже.",
state=state,
reply_markup=keyboard.as_markup()
)
return
min_price_usd = min(prices)
max_price_usd = max(prices)
min_price_rub = min_price_usd * usd_to_rub_rate if usd_to_rub_rate else 0
max_price_rub = max_price_usd * usd_to_rub_rate if usd_to_rub_rate else 0
operator_display = OPERATOR_DISPLAY_NAMES.get(user_operator, user_operator)
service_display = SERVICE_DISPLAY_NAMES.get(service_code, service_code)
keyboard = InlineKeyboardBuilder()
keyboard.button(text=f"⬇️ Мин. цена ({min_price_usd:.4f}$)", callback_data=f"select_price:{min_price_usd}")
keyboard.button(text=f"⬆️ Макс. цена ({max_price_usd:.4f}$)", callback_data=f"select_price:{max_price_usd}")
keyboard.adjust(2)
keyboard.row(types.InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_operation"))
message_text = (
f"*Страна:* `{country_name}`\n"
f"*Доступно номеров:* `{available_numbers}`\n"
f"*Оператор:* `{operator_display}`\n"
f"*Сервис:* `{service_display}`\n"
f"*Цены:* от `{min_price_usd:.4f}` $ (₽{min_price_rub:.2f}) до `{max_price_usd:.4f}` $ (₽{max_price_rub:.2f}).\n\n"
f"_Выберите максимальную допустимую стоимость покупки с помощью кнопок ниже или введите сумму вручную (например: 0.5). Все суммы указаны в долларах ($):_"
)
await safe_edit_or_send_message(
message=message,
new_text=message_text,
state=state,
reply_markup=keyboard.as_markup(),
parse_mode="Markdown"
)
await state.update_data(
service=service_code,
service_display=service_display,
country_name=country_name,
available_numbers=available_numbers,
operator_display=operator_display,
min_price=min_price_usd,
max_price_usd=max_price_usd,
min_price_rub=min_price_rub,
max_price_rub=max_price_rub,
usd_to_rub_rate=usd_to_rub_rate,
user_id=user_id
)
await state.set_state(SmsOrderState.waiting_for_max_price)
success = True
break
except Exception as e:
logger.error(f"Ошибка при получении данных для сервиса '{service_code}', попытка {attempt}: {e}")
last_exception = e
await asyncio.sleep(5)
except asyncio.CancelledError:
logger.info(f"Задача для пользователя {user_id} была отменена.")
return
if not success:
keyboard = InlineKeyboardBuilder()
keyboard.button(text="Назад", callback_data="service_page:0")
await safe_edit_or_send_message(
message=message,
new_text=f"Произошла ошибка при обработке запроса: {last_exception}. Попробуйте снова.",
state=state,
reply_markup=keyboard.as_markup()
)
async def price_selected_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
price = float(callback_query.data.split(":")[1])
await state.update_data(max_price=price)
await process_number_purchase(callback_query.message, state)
async def max_price_handler(message: types.Message, state: FSMContext):
max_price_input = message.text.strip()
data = await state.get_data()
user_id = data.get('user_id', message.from_user.id)
try:
await message.delete()
except Exception as e:
logger.error(f"Не удалось удалить сообщение пользователя: {e}")
logger.info(f"Пользователь {user_id} указал максимальную стоимость: {max_price_input}")
try:
max_price = float(max_price_input)
except ValueError:
user_data = await state.get_data()
min_price = user_data.get('min_price')
max_price_usd = user_data.get('max_price_usd')
min_price_rub = user_data.get('min_price_rub')
max_price_rub = user_data.get('max_price_rub')
keyboard = InlineKeyboardBuilder()
keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=message,
new_text=(
f"Пожалуйста, введите корректное число для максимальной стоимости. "
f"От `{min_price:.4f}` $ (₽{min_price_rub:.2f}) до `{max_price_usd:.4f}` $ (₽{max_price_rub:.2f})."
),
state=state,
reply_markup=keyboard.as_markup(),
parse_mode="Markdown"
)
return
if max_price < data.get('min_price'):
keyboard = InlineKeyboardBuilder()
keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=message,
new_text=f"Максимальная стоимость должна быть не менее `{data.get('min_price'):.4f}` $.",
state=state,
reply_markup=keyboard.as_markup(),
parse_mode="Markdown"
)
return
await state.update_data(max_price=max_price)
await process_number_purchase(message, state)
async def process_number_purchase(message: types.Message, state: FSMContext):
user_data = await state.get_data()
user_id = user_data.get('user_id', message.from_user.id)
service = user_data['service']
service_display = user_data['service_display']
country_name = user_data['country_name']
operator_display = user_data['operator_display']
max_price = user_data['max_price']
user_api_key = get_user_api_key(user_id)
smshub_api = SmsHubAPI(user_api_key)
user_country_code = get_user_country(user_id) or "0"
max_attempts = 3
last_exception = None
for attempt in range(1, max_attempts + 1):
data = await state.get_data()
if data.get("cancel_request"):
logger.info(f"Запрос на получение номера для пользователя {user_id} был отменен.")
return
try:
progress_text = f"Идет запрос {attempt}/{max_attempts} на получение номера..."
keyboard = InlineKeyboardBuilder()
keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=message,
new_text=progress_text,
state=state,
reply_markup=keyboard.as_markup()
)
user_operator = get_user_operator(user_id) or "any"
result = await smshub_api.get_number(service, country=user_country_code, max_price=max_price, operator=user_operator)
if result and result.startswith("ACCESS_NUMBER"):
_, activation_id, number = result.split(":")
logger.info(f"Пользователь {user_id} получил номер: {number}, активация ID: {activation_id}")
if user_id not in user_activations:
user_activations[user_id] = {}
user_activations[user_id][activation_id] = {
'number': number,
'status': 'Ожидание SMS',
'service': service,
'service_display': service_display,
'country_name': country_name,
'last_update': time.time(),
'task': None
}
keyboard = InlineKeyboardBuilder()
keyboard.button(text="📂 Текущие активации", callback_data="current_activations")
keyboard.button(text="❌ Отменить активацию", callback_data=f"cancel_activation:{activation_id}")
keyboard.button(text="🔙 Главное меню", callback_data="back_to_main_menu")
keyboard.adjust(1)
message_text = (
f"*Получен номер:* `{number}`\n"
f"*Страна:* `{country_name}`\n"
f"*Оператор:* `{operator_display}`\n"
f"*Сервис:* `{service_display}`\n"
f"*Максимальная стоимость:* `{max_price:.2f}` $ (₽{max_price * user_data['usd_to_rub_rate']:.2f})\n"
f"*Статус:* *Ожидание SMS*\n"
f"Вы можете управлять активацией или вернуться в главное меню."
)
await safe_edit_or_send_message(
message=message,
new_text=message_text,
state=state,
reply_markup=keyboard.as_markup(),
parse_mode="Markdown"
)
await state.set_state(None)
task = asyncio.create_task(poll_for_sms(
bot=message.bot,
user_id=user_id,
chat_id=message.chat.id,
activation_id=activation_id,
smshub_api=smshub_api
))
user_activations[user_id][activation_id]['task'] = task
return # Успешно получили номер, выходим
elif result == "NO_NUMBERS":
logger.info(f"Попытка {attempt} из {max_attempts}: нет доступных номеров.")
last_exception = result
if attempt == max_attempts:
suggestion = (
"Попробуйте выбрать 'Любой оператор' в настройках или увеличить максимальную цену."
if user_operator != "any" else
"Попробуйте увеличить максимальную цену или повторите попытку позже."
)
keyboard = InlineKeyboardBuilder()
keyboard.button(text="Назад", callback_data="service_page:0")
await safe_edit_or_send_message(
message=message,
new_text=f"Нет доступных номеров для оператора {operator_display} по данной цене.\n{suggestion}",
state=state,
reply_markup=keyboard.as_markup()
)
else:
raise Exception("Неожиданный ответ API")
except Exception as e:
logger.error(f"Ошибка при попытке получения номера, попытка {attempt}: {e}")
last_exception = e
await asyncio.sleep(5) # Задержка перед следующей попыткой
if last_exception and last_exception != "NO_NUMBERS":
keyboard = InlineKeyboardBuilder()
keyboard.button(text="Назад", callback_data="service_page:0")
await safe_edit_or_send_message(
message=message,
new_text=f"Произошла ошибка при обработке запроса: {last_exception}. Попробуйте снова.",
state=state,
reply_markup=keyboard.as_markup()
)
def register_sms_handlers(dp: Dispatcher):
dp.callback_query.register(get_sms_handler, F.data == "get_sms")
dp.callback_query.register(go_back_to_service_selection, F.data == "cancel_operation")
dp.callback_query.register(back_to_main_menu, F.data == "back_to_main_menu")
dp.callback_query.register(service_page_handler, F.data.startswith("service_page:"))
dp.callback_query.register(service_selected_handler, F.data.startswith("service_select:"))
dp.callback_query.register(price_selected_handler, F.data.startswith("select_price:"))
dp.message.register(service_handler, SmsOrderState.waiting_for_service)
dp.message.register(max_price_handler, SmsOrderState.waiting_for_max_price)
# bot/handlers/start.py
import logging
from aiogram import Dispatcher, types
from aiogram.filters import Command
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from ..data import set_user_operator, get_user_operator
from ..utils import safe_delete_message, safe_edit_or_send_message
logger = logging.getLogger(__name__)
async def start_handler(message: types.Message, state: FSMContext):
logger.info(f"Пользователь {message.from_user.id} запустил бота.")
try:
await safe_delete_message(message.bot, message.chat.id, message.message_id)
except Exception as e:
logger.error(f"Ошибка при удалении сообщения с командой /start: {e}")
user_operator = get_user_operator(message.from_user.id)
if not user_operator:
set_user_operator(message.from_user.id, "any")
await send_main_menu(message, state)
async def send_main_menu(message: types.Message, state: FSMContext):
logger.info(f"Пользователь {message.chat.id} вернулся в главное меню.")
keyboard = InlineKeyboardBuilder()
keyboard.button(text="💰 Проверить баланс", callback_data="check_balance")
keyboard.button(text="📲 Получить SMS", callback_data="get_sms")
keyboard.button(text="📋 Текущие активации", callback_data="current_activations")
keyboard.button(text="⚙️ Настройки", callback_data="settings")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=message,
new_text="Добро пожаловать! Выберите действие:",
state=state,
reply_markup=keyboard.as_markup()
)
def register_start_handlers(dp: Dispatcher): # Изменено название функции
dp.message.register(start_handler, Command('start'))ASSISTANT
Для улучшения структуры вашего проекта и увеличения его поддерживаемости, можно внести несколько изменений. Основная цель — разделить код на более мелкие и логически взаимосвязанные части, чтобы упростить навигацию и поддержку в будущем. Также важно придерживаться принципов DRY (Don't Repeat Yourself) и KISS (Keep It Simple, Stupid).
Вот предложения для улучшения:
1. **Разделение обработчиков по функциональности в отдельные модули.**
Обработчики можно разбить на несколько модулей по логике их работы. Это поможет лучше организовать структуру проекта и проще управлять отдельными частями. Например:
- `bot/handlers/activation/` для активации.
- `bot/handlers/balance/` для проверки баланса.
- `bot/handlers/settings/` для настроек пользователя.
- `bot/handlers/errors/` для обработки ошибок.
- `bot/handlers/start/` для начальных команд и отправки главного меню.
- `bot/handlers/sms/` для управления SMS.
2. **Перенос логики в отдельные функции или классы.**
Операции по взаимодействию с внешними API (например, SmsHubAPI) можно вынести отдельно, чтобы улучшить читаемость кода и упростить тестирование этих функций.
3. **Улучшение структуры обработчиков.**
В каждом обработчике переработать логику таким образом, чтобы максимально разделить их на более мелкие функции, которые отвечают за конкретные задачи.
4. **Использование типизации.**
Добавление более четкой типизации поможет избежать ошибок и улучшит поддержку кода.
Пример улучшенной структуры файлов:
```
bot/
└── handlers/
├── __init__.py
├── activation/
│ ├── __init__.py
│ ├── activation_handling.py
│ └── polling.py
├── balance/
│ ├── __init__.py
│ └── balance_handling.py
├── settings/
│ ├── __init__.py
│ ├── country_selection.py
│ ├── operator_selection.py
│ ├── api_key_handling.py
│ └── settings_menu.py
├── sms/
│ ├── __init__.py
│ └── sms_handling.py
├── errors/
│ ├── __init__.py
│ └── error_handling.py
├── start/
│ ├── __init__.py
│ └── start_handling.py
```
Пример нового содержимого некоторых файлов:
**`bot/handlers/__init__.py`:** регистрируйте обработчики, импортируя их из соответствующих модулей.
```python
from aiogram import Dispatcher
from .activation import register_activation_handlers
from .balance import register_balance_handlers
from .settings import register_settings_handlers
from .sms import register_sms_handlers
from .errors import register_error_handlers
from .start import register_start_handlers
def register_handlers(dp: Dispatcher):
register_activation_handlers(dp)
register_balance_handlers(dp)
register_settings_handlers(dp)
register_sms_handlers(dp)
register_error_handlers(dp)
register_start_handlers(dp)
```
**`bot/handlers/activation/__init__.py`:**
```python
from aiogram import Dispatcher
from .activation_handling import current_activations_handler, manage_activation_handler
from .polling import poll_for_sms
def register_activation_handlers(dp: Dispatcher):
dp.callback_query.register(current_activations_handler, lambda cq: "current_activations" in cq.data)
dp.callback_query.register(manage_activation_handler, lambda cq: "manage_activation:" in cq.data)
```
**`bot/handlers/errors/__init__.py`:**
```python
from aiogram import Dispatcher
from .error_handling import errors_handler
def register_error_handlers(dp: Dispatcher):
dp.errors.register(errors_handler)
```
Каждый модуль в директории handlers будет иметь аналогичную структуру, разделенную по функциональности. Такой подход упрощает понимание кода и делает его более модульным, облегчая тестирование и внесение изменений в будущем.