У меня есть телеграм бот для работы с сервисом SMSHUB через пи, это сервис для покупки виртуальных номеров и для получения на них смс и отправки пользователю. У тебя етсь идеи, как улучшить моего бота? Предложи что-то дельное, можешь изменить код и прислать мне готовый вариант. Я высылаю тебе полный код моего бота и так же документацию по работе через апи с сервисом. в ответе сразу мне отправь так же полностью готовый код в текстовом виде для файлов, со структурой (возможно ты захочешь некоторые файлы разделить на более маленькие, либо вообще захочешь изменить струтуру). Если ты захочешь к примеру добавить базу данных, то так же тогда в начале напииши полную инструкцию, как это все дело реализовать.

документация: 
Внимание! Добавлена поддержка валюты.

Методы getBalance, getNumber, getPrices теперь принимают параметр currency

Параметр currency принимает два значения 643, 840 (цифровой код валюты ₽ и $ по стандарту ISO)

Также добавлен новый метод updateApiCurrency, который задает значение по умолчанию, если currency не передано

Если в метод не передан параметр currency и не было задано значение с помощью updateApiCurrency, мы используем значение по умолчанию заданное в нашей системе

API - это протокол взаимодействия между вашим ПО и нашим сервером активаций.

API нужен для того, чтобы автоматизировать процесс получение смс сообщений на вашей стороне

Все запросы (поддерживаются POST и GET) должны идти на https://smshub.org/stubs/handler_api.php

Во всех запросах должен присутствовать API ключ в виде параметра "api_key"

Запрос количества доступных номеров:
https://smshub.org/stubs/handler_api.php?api_key=APIKEY&action=getNumbersStatus&country=COUNTRY&operator=OPERATOR
Входные параметры
Параметр	Принимаемые значения	Обязательность	Описание
APIKEY	APIKEY	Да	Ваш API-ключ, обеспечивает доступ к оплаченным услугам. API-ключ следует хранить в тайне
COUNTRY	См. список стран в левом меню	Нет	Страна номера, если не задан, то будет выбран номер той страны, которая последний раз была выбрана в левом меню
OPERATOR	Список доступных операторов можно найти в левом меню соответсвующей страны	Нет	Оператор номера, если не задан, то будет выбран тот оператор, который последний раз была выбран в левом меню
Ответ сервиса будет в json формате, пример:

{"apd_0":200,"amy_0":249,"aly_0":246,"tu_0":214,"aho_0":115,"ky_0":86,"apn_0":214,"ajs_0":66,"yd_0":205,"ayc_0":27}

Где до черточки - название сервиса, после обозначение нужно ли включать переадресацию. 0 - не включать. 1 - включать. Номеров с переадресацией мы к сожалению пока не предоставляем

Запрос баланса
https://smshub.org/stubs/handler_api.php?api_key=APIKEY&action=getBalance&currency=CURRENCY
Входные параметры
Параметр	Принимаемые значения	Обязательность	Описание
APIKEY	APIKEY	Да	Ваш API-ключ, обеспечивает доступ к оплаченным услугам. API-ключ следует хранить в тайне
CURRENCY	643, 840	Нет	Если передано - возвращает значение соответствующего баланса.
Выходные данные
Ответ сервера	Пример	Описание
ACCESS_BALANCE:BALANCE	ACCESS_BALANCE:540	BALANCE - Баланс вашего API-ключа
Возможные ошибки
Ответ сервера	Описание
BAD_KEY	Неверный API-ключ
ERROR_SQL	ошибка SQL-сервера
BAD_ACTION	Общее неправильное формирование запроса
BAD_CURRENCY	Неверное значение currency
Заказ номера
https://smshub.org/stubs/handler_api.php?api_key=APIKEY&action=getNumber&service=SERVICE&operator=OPERATOR&country=COUNTRY&maxPrice=MAXPRICE&currency=CURRENCY
Входные параметры
Параметр	Принимаемые значения	Обязательность	Описание
APIKEY	APIKEY	Да	Ваш API-ключ, обеспечивает доступ к оплаченным услугам. API-ключ следует хранить в тайне
SERVICE	См. список сервисов в левом меню	Да	Сервис, номер для которого необходимо получить
OPERATOR	Принимаемые параметры зависят от конкретной страны, для уточнения обратите внимание на левое меню	Нет	Сотовый оператор номер которого необходимо получить
COUNTRY	См. список стран в левом меню	Нет	Страна номер которой необходимо получить
MAXPRICE	Числовое значение	Нет	Максимальная цена, за которую вы готовы купить номер по Free Price
CURRENCY	643, 840	Нет	С какого баланса будет списана стоимость номера.
Ответы сервиса
Ответ сервера	Пример	Описание
NO_NUMBERS	NO_NUMBERS	Нет номеров с заданными параметрами, попробуйте позже, или поменяйте оператора, страну.
NO_BALANCE	NO_BALANCE	Закончились деньги на API-ключе
API_KEY_NOT_VALID	API_KEY_NOT_VALID	Невалидный статус API-ключа
WRONG_SERVICE	WRONG_SERVICE	Не верный идентификатор сервиса
ACCESS_NUMBER:ID:NUMBER	ACCESS_NUMBER:234242:79123456789	Получили номер, ID активации - ID, сам номер с кодом страны - NUMBER
Возможные ошибки
Ответ сервера	Описание
BAD_ACTION	Общее неправильное формирование запросаО
BAD_SERVICE	некорректное наименование сервиса
BAD_KEY	Неверный API-ключ
ERROR_SQL	Ошибка базы SQL-сервера
BAD_CURRENCY	Неверное значение currency
Изменить статус
https://smshub.org/stubs/handler_api.php?api_key=APIKEY&action=setStatus&status=STATUS&id=ID
Входные параметры
Параметр	Принимаемые значения	Обязательность	Описание
APIKEY	APIKEY	Да	Ваш API-ключ, обеспечивает доступ к оплаченным услугам. API-ключ следует хранить в тайне
ID	Числовое значение	Да	ID активации, полученное при запросе номера
STATUS	1 - смс на номер отправлено, 3 - необходимо повторно смс, 6 - активация успешно завершена, 8 - отменить активацию	Да	Статус который необходимо передать активации
Какой статус когда отправлять?

Простая логика взаимодействия с API

Сразу после получения номера доступны следующие действия:
8 - Отменить активацию
1 - Сообщить, что SMS отправлена (необязательно)
Для активации со статусом 1:
8 - Отменить активацию
Сразу после получения кода:
3 - Запросить еще одну смс
6 - Подтвердить SMS-код и завершить активацию
Для активации со статусом 3:
6 - Подтвердить SMS-код и завершить активацию
Выходные данные
Ответ сервера	Описание
ACCESS_READY	Готовность ожидания смс
ACCESS_RETRY_GET	Ожидаем новое смс
ACCESS_ACTIVATION	Активация успешно завершена
ACCESS_CANCEL	Активация отменена
Возможные ошибки
Ответ сервера	Описание
BAD_ACTION	Общее неправильное формирование запроса
BAD_SERVICE	некорректное наименование сервиса
BAD_KEY	Неверный API-ключ
NO_ACTIVATION	id активации не существует
ERROR_SQL	Ошибка базы SQL-сервера, обратитесь к администратору
Получить статус
https://smshub.org/stubs/handler_api.php?api_key=APIKEY&action=getStatus&id=ID
Входные параметры
Параметр	Принимаемые значения	Обязательность	Описание
APIKEY	APIKEY	Да	Ваш API-ключ, обеспечивает доступ к оплаченным услугам. API-ключ следует хранить в тайне
ID	Числовое значение	Да	ID активации, полученное при запросе номера
Выходные данные
Ответ сервера	Описание
STATUS_WAIT_CODE	Ожидаем прихода смс
STATUS_WAIT_RETRY:LASTCODE	Ожидаем еще одно смс, LASTCODE - последнее полученное смс
STATUS_CANCEL	активация отменена
STATUS_OK:CODE	Код получен (где CODE - код активации)
Возможные ошибки
Ответ сервера	Описание
BAD_ACTION	Общее неправильное формирование запроса
BAD_KEY	Неверный API-ключ
NO_ACTIVATION	id активации не существует
ERROR_SQL	Ошибка базы SQL-сервера, обратитесь к администратору
Запросить все цены
https://smshub.org/stubs/handler_api.php?api_key=APIKEY&action=getPrices&service=SERVICE&country=COUNTRY&currency=CURRENCY
Входные параметры
Параметр	Принимаемые значения	Обязательность	Описание
APIKEY	APIKEY	Да	Ваш API-ключ, обеспечивает доступ к оплаченным услугам. API-ключ следует хранить в тайне
SERVICE	См. список сервисов в левом меню	Нет	Сервис, номер для которого необходимо получить
COUNTRY	См. список стран в левом меню	Нет	Страна номера
CURRENCY	643, 840	Нет	Если передано - метод вернет стоимость в соответствующей валюте.
Выходные данные
Ответ сервера	Описание
JSON ответ	Ответ в формате { "Страна": { "Сервис": { "Цена": Количество}}}
Возможные ошибки
Ответ сервера	Описание
BAD_CURRENCY	Неверное значение currency
Изменить валюту по умолчанию
https://smshub.org/stubs/handler_api.php?api_key=APIKEY&action=updateApiCurrency&&currency=CURRENCY
Входные параметры
Параметр	Принимаемые значения	Обязательность	Описание
APIKEY	APIKEY	Да	Ваш API-ключ, обеспечивает доступ к оплаченным услугам. API-ключ следует хранить в тайне
CURRENCY	643, 840	Да	Значение валюты которое будет установлено по умолчанию для запросов по API.
Выходные данные
Ответ сервера	Описание
JSON ответ	Ответ в формате {"status":"success"}
Возможные ошибки
Ответ сервера	Описание
BAD_CURRENCY	Неверное значение currency
CURRENCY_CHANGE_UNAVAILABLE	Изменение валюты недоступно








# /config.py

import os
from dotenv import load_dotenv

load_dotenv()

API_KEY = os.getenv("API_KEY")
TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
ADMIN_CHAT_ID = int(os.getenv("ADMIN_CHAT_ID", "389669884"))





# /main.py

import asyncio
import logging
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from config import TELEGRAM_TOKEN
from bot.handlers import register_handlers
from bot.utils import on_startup
from aiogram.client.bot import DefaultBotProperties

# Настройка логирования
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)

async def main():
    bot = Bot(token=TELEGRAM_TOKEN, default=DefaultBotProperties(parse_mode='HTML'))
    storage = MemoryStorage()
    dp = Dispatcher(storage=storage)

    # Регистрация обработчиков
    register_handlers(dp)

    # Регистрация функции on_startup
    dp.startup.register(on_startup)

    try:
        logger.info("Запуск бота...")
        await dp.start_polling(bot)
    finally:
        await bot.close()

if __name__ == "__main__":
    asyncio.run(main())
	
	
	
	
	
	
	
	# /requirements.txt

python-dotenv
aiogram
requests






# /bot/api.py

import aiohttp
import logging
import json
from typing import Optional, Dict

logger = logging.getLogger(__name__)

BASE_URL = "https://smshub.org/stubs/handler_api.php"

class SmsHubAPI:
    def __init__(self, api_key: str):
        self.api_key = api_key

    async def get_balance(self, currency: str = "643") -> Optional[str]:
        """Запрос баланса с заданной валютой (по умолчанию: RUB)."""
        logger.info(f"Запрос баланса с валютой: {currency}")
        params = {
            "api_key": self.api_key,
            "action": "getBalance",
            "currency": currency
        }
        async with aiohttp.ClientSession() as session:
            async with session.get(BASE_URL, params=params) as response:
                if response.status == 200:
                    text = await response.text()
                    logger.info(f"Ответ баланса: {text}")
                    return text
                else:
                    logger.error(f"Ошибка при запросе баланса: {response.status} - {text}")
                    return None

    async def get_number(self, service: str, country: str, operator: Optional[str] = None, max_price: Optional[float] = None, currency: str = "643") -> Optional[str]:
        """Получение номера для указанного сервиса и страны."""
        logger.info(f"Запрос номера для сервиса: {service}, страна: {country}, оператор: {operator}, макс. цена: {max_price}, валюта: {currency}")
        params = {
            "api_key": self.api_key,
            "action": "getNumber",
            "service": service,
            "country": country,
            "currency": currency
        }
        if operator and operator != "any":
            params["operator"] = operator
        if max_price:
            params["maxPrice"] = max_price

        async with aiohttp.ClientSession() as session:
            async with session.get(BASE_URL, params=params) as response:
                text = await response.text()
                if response.status == 200:
                    logger.info(f"Ответ номера: {text}")
                    return text
                logger.error(f"Ошибка при запросе номера: {response.status} - {text}")
                return None

    async def get_price_for_service(self, service: str, country: str, currency: str = "643") -> Optional[Dict]:
        """Получение цен для указанного сервиса."""
        logger.info(f"Запрос цен для сервиса: {service}, страна: {country}, валюта: {currency}")
        params = {
            "api_key": self.api_key,
            "action": "getPrices",
            "service": service,
            "country": country,
            "currency": currency
        }

        async with aiohttp.ClientSession() as session:
            async with session.get(BASE_URL, params=params) as response:
                response_text = await response.text()
                if response.status == 200:
                    try:
                        prices_data = json.loads(response_text)
                        logger.info(f"Цены для сервиса {service}: {prices_data}")
                        country_data = prices_data.get(country)
                        if not country_data:
                            logger.error(f"Страна '{country}' отсутствует в данных: {prices_data}")
                            return None
                        service_data = country_data.get(service)
                        if not service_data:
                            logger.error(f"Сервис '{service}' отсутствует среди доступных для страны '{country}'. Данные: {country_data}")
                            return None
                        return service_data  # Возвращает словарь { "Цена": Количество }
                    except json.JSONDecodeError:
                        logger.error(f"Ошибка парсинга JSON: {response_text}")
                        return None
                else:
                    logger.error(f"Ошибка при запросе цен: {response.status} - {response_text}")
                    return None

    async def get_numbers_status(self, country: str, operator: Optional[str] = None) -> Optional[Dict]:
        """Получение статуса номеров по стране и оператору."""
        logger.info(f"Запрос статуса номеров для страны: {country}, оператор: {operator}")
        params = {
            "api_key": self.api_key,
            "action": "getNumbersStatus",
            "country": country
        }
        if operator and operator != "any":
            params["operator"] = operator
        async with aiohttp.ClientSession() as session:
            async with session.get(BASE_URL, params=params) as response:
                response_text = await response.text()
                if response.status == 200:
                    try:
                        numbers_status = json.loads(response_text)
                        logger.info(f"Статус номеров: {numbers_status}")
                        return numbers_status  # Возвращает словарь { "SERVICE_CODE": Количество }
                    except json.JSONDecodeError:
                        logger.error(f"Ошибка парсинга JSON: {response_text}")
                        return None
                else:
                    logger.error(f"Ошибка при запросе статуса номеров: {response.status} - {response_text}")
                    return None

    async def get_status(self, activation_id: str) -> Optional[str]:
        """Получение статуса активации по ID."""
        logger.info(f"Запрос статуса для активации: {activation_id}")
        params = {
            "api_key": self.api_key,
            "action": "getStatus",
            "id": activation_id
        }
        async with aiohttp.ClientSession() as session:
            async with session.get(BASE_URL, params=params) as response:
                text = await response.text()
                if response.status == 200:
                    logger.info(f"Ответ статуса: {text}")
                    return text
                logger.error(f"Ошибка при запросе статуса: {response.status} - {text}")
                return None

    async def set_status(self, activation_id: str, status: str) -> Optional[str]:
        """Установка статуса для активации."""
        logger.info(f"Изменение статуса активации: {activation_id} на статус {status}")
        params = {
            "api_key": self.api_key,
            "action": "setStatus",
            "id": activation_id,
            "status": status
        }
        async with aiohttp.ClientSession() as session:
            async with session.get(BASE_URL, params=params) as response:
                text = await response.text()
                if response.status == 200:
                    logger.info(f"Ответ изменения статуса: {text}")
                    return text
                logger.error(f"Ошибка при изменении статуса: {response.status} - {text}")
                return None

    async def cancel_activation(self, activation_id: str) -> Optional[str]:
        """Отмена активации."""
        return await self.set_status(activation_id, status="8")
		
		
		
		
		
		
		
		
		
		
		
		
		# /bot/data.py

import json
import os
import threading
import logging
from typing import Optional, Dict, Any

logger = logging.getLogger(__name__)

# Путь к файлу для хранения данных пользователей
DATA_FILE = os.path.join(os.path.dirname(__file__), "data.json")
# Путь к файлу для хранения истории API ключей
HISTORY_FILE = os.path.join(os.path.dirname(__file__), "history.json")
lock = threading.RLock()

# Загружаем данные из файла при запуске
user_data: Dict[str, Any] = {}
api_key_history: Dict[int, list] = {}  # Словарь для хранения истории API ключей


def load_data() -> None:
    """Загружает основные данные и историю из JSON файлов."""
    global user_data, api_key_history
    with lock:
        if os.path.exists(DATA_FILE):
            try:
                with open(DATA_FILE, "r", encoding="utf-8") as file:
                    user_data = json.load(file)
                    logger.info("Данные пользователей успешно загружены.")
            except json.JSONDecodeError:
                user_data = {}
                logger.warning("Не удалось декодировать JSON из файла. Используем пустые данные.")
        else:
            user_data = {}
            logger.info("Файл данных не найден. Используем пустые данные.")
        
        if os.path.exists(HISTORY_FILE):
            try:
                with open(HISTORY_FILE, "r", encoding="utf-8") as file:
                    api_key_history = json.load(file)
                    logger.info("История API ключей успешно загружена.")
            except json.JSONDecodeError:
                api_key_history = {}
                logger.warning("Не удалось декодировать JSON из файла истории. Используем пустые данные.")
        else:
            api_key_history = {}
            logger.info("Файл истории не найден. Используем пустые данные.")


# Вызываем load_data() при импорте модуля
load_data()


def save_data() -> None:
    """Сохраняет текущие данные в JSON файл."""
    with lock:
        with open(DATA_FILE, "w", encoding="utf-8") as file:
            json.dump(user_data, file, ensure_ascii=False, indent=4)
            logger.info("Данные пользователей успешно сохранены.")


def save_history() -> None:
    """Сохраняет историю API ключей в файл."""
    with lock:
        with open(HISTORY_FILE, "w", encoding="utf-8") as file:
            json.dump(api_key_history, file, ensure_ascii=False, indent=4)
            logger.info("История API ключей успешно сохранена.")


# Методы для работы с API ключами
def set_user_api_key(user_id: int, api_key: str) -> None:
    """Сохраняет API ключ для конкретного пользователя."""
    with lock:
        user = user_data.get(str(user_id), {})
        user["api_key"] = api_key
        user_data[str(user_id)] = user
        save_data()
        logger.info(f"API ключ для пользователя {user_id} сохранён.")

        # Сохраняем в историю
        api_key_history.setdefault(user_id, []).append(api_key)
        save_history()


def get_user_api_key(user_id: int) -> Optional[str]:
    """Возвращает API ключ для конкретного пользователя, если он сохранен."""
    with lock:
        return user_data.get(str(user_id), {}).get("api_key")


def delete_user_api_key(user_id: int) -> None:
    """Удаляет API ключ для конкретного пользователя."""
    with lock:
        user = user_data.get(str(user_id), {})
        if "api_key" in user:
            # Записываем удаляемый ключ в историю
            api_key_history.setdefault(user_id, []).append(user["api_key"])
            del user["api_key"]
            if not user:
                del user_data[str(user_id)]
            save_data()
            save_history()
            logger.info(f"API ключ для пользователя {user_id} удалён.")
        else:
            logger.warning(f"У пользователя {user_id} нет API ключа для удаления.")


# Методы для работы с оператором
def set_user_operator(user_id: int, operator: str) -> None:
    """Сохраняет выбранный оператор для конкретного пользователя."""
    with lock:
        user = user_data.get(str(user_id), {})
        user["operator"] = operator
        user_data[str(user_id)] = user
        save_data()
        logger.info(f"Оператор '{operator}' для пользователя {user_id} сохранён.")


def get_user_operator(user_id: int) -> Optional[str]:
    """Возвращает выбранный оператор для конкретного пользователя, если он сохранен."""
    with lock:
        return user_data.get(str(user_id), {}).get("operator")


def delete_user_operator(user_id: int) -> None:
    """Удаляет выбранный оператор для конкретного пользователя."""
    with lock:
        user = user_data.get(str(user_id), {})
        if "operator" in user:
            del user["operator"]
            if not user:
                del user_data[str(user_id)]
            save_data()
            logger.info(f"Оператор для пользователя {user_id} удалён.")
        else:
            logger.warning(f"У пользователя {user_id} нет оператора для удаления.")


# Методы для работы со страной
def set_user_country(user_id: int, country: str) -> None:
    """Сохраняет выбранную страну для конкретного пользователя."""
    with lock:
        user = user_data.get(str(user_id), {})
        user["country"] = country
        user_data[str(user_id)] = user
        save_data()
        logger.info(f"Страна '{country}' для пользователя {user_id} сохранена.")


def get_user_country(user_id: int) -> Optional[str]:
    """Возвращает выбранную страну для конкретного пользователя, если она сохранена."""
    with lock:
        return user_data.get(str(user_id), {}).get("country")


def delete_user_country(user_id: int) -> None:
    """Удаляет выбранную страну для конкретного пользователя."""
    with lock:
        user = user_data.get(str(user_id), {})
        if "country" in user:
            del user["country"]
            if not user:
                del user_data[str(user_id)]
            save_data()
            logger.info(f"Страна для пользователя {user_id} удалена.")
        else:
            logger.warning(f"У пользователя {user_id} нет страны для удаления.")
			
			
			
			
			
			
			
			
			
			
			
			# /bot/utils.py

import logging
import time
import aiohttp
from aiogram import Bot, types
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
from aiogram import Dispatcher
from typing import Optional

logger = logging.getLogger(__name__)

EXCHANGE_RATE_API_URL = "https://api.exchangerate-api.com/v4/latest/USD"
USD_TO_RUB_RATE_TTL = 3600

_cached_usd_to_rub_rate: Optional[float] = None
_cached_usd_to_rub_rate_time: float = 0.0

async def get_usd_to_rub_rate() -> Optional[float]:
    global _cached_usd_to_rub_rate, _cached_usd_to_rub_rate_time
    now = time.time()
    if _cached_usd_to_rub_rate and (now - _cached_usd_to_rub_rate_time < USD_TO_RUB_RATE_TTL):
        return _cached_usd_to_rub_rate
    try:
        async with aiohttp.ClientSession() as session:
            async with session.get(EXCHANGE_RATE_API_URL) as response:
                data = await response.json()
                rate = data.get('rates', {}).get('RUB')
                if rate:
                    _cached_usd_to_rub_rate = rate
                    _cached_usd_to_rub_rate_time = now
                    return rate
                else:
                    logger.error("Курс RUB не найден в ответе.")
                    return None
    except Exception as e:
        logger.error(f"Ошибка при запросе курса валют: {e}")
        return None

async def safe_edit_or_send_message(
    message: types.Message,
    new_text: str,
    state: FSMContext,
    **kwargs
) -> Optional[types.Message]:
    data = await state.get_data()
    last_message_id = data.get('last_message_id')
    last_message_text = data.get('last_message_text')
    last_reply_markup = data.get('last_reply_markup')

    new_reply_markup = kwargs.get("reply_markup")

    if last_message_id and (last_message_text != new_text or last_reply_markup != new_reply_markup):
        try:
            sent_message = await message.bot.edit_message_text(
                new_text,
                chat_id=message.chat.id,
                message_id=last_message_id,
                **kwargs
            )
            await state.update_data(last_message_text=new_text, last_reply_markup=new_reply_markup)
            return sent_message
        except TelegramBadRequest as e:
            if "message can't be edited" in str(e).lower():
                logger.warning(f"Сообщение не может быть отредактировано: {e}. Пробуем отправить новое сообщение.")
            else:
                logger.error(f"Ошибка при редактировании сообщения: {e}")
        except Exception as e:
            logger.exception(f"Неожиданная ошибка при редактировании сообщения: {e}")

    if last_message_id:
        try:
            await message.bot.delete_message(chat_id=message.chat.id, message_id=last_message_id)
        except Exception as del_exc:
            logger.error(f"Не удалось удалить сообщение: {del_exc}")

    try:
        sent_message = await message.answer(new_text, **kwargs)
        await state.update_data(
            last_message_id=sent_message.message_id,
            last_message_text=new_text,
            last_reply_markup=new_reply_markup
        )
        return sent_message
    except Exception as e:
        logger.error(f"Не удалось отправить новое сообщение: {e}")
        return None

async def safe_delete_message(bot: Bot, chat_id: int, message_id: int) -> None:
    try:
        await bot.delete_message(chat_id=chat_id, message_id=message_id)
    except Exception as e:
        logger.error(f"Не удалось удалить сообщение: {e}")

async def on_startup(dispatcher: Dispatcher):
    logger.info("Бот успешно запущен.")
	
	
	
	
	
	
	
	
	
	
	
	# /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)
	
	
	
	
	
	
	
	
	
	
	
	# /bot/handlers/activation.py

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 ..constants.services 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, {})

    if not activations:
        keyboard = InlineKeyboardBuilder()
        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

    keyboard = InlineKeyboardBuilder()
    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 in ["STATUS_WAIT_CODE", "STATUS_WAIT_RETRY"]:
                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 "Код не получен"
                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)

                # Отправляем новое сообщение и сохраняем его message_id
                sent_message = await bot.send_message(
                    chat_id=chat_id,
                    text=f"Код активации для номера `{activation['number']}`: `{code}`",
                    reply_markup=keyboard.as_markup(),
                    parse_mode="Markdown"
                )
                
                # Сохраняем новое message_id
                activation['message_id'] = sent_message.message_id

                return

            else:
                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  # Возвращаем 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

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.operators import OPERATOR_DISPLAY_NAMES
from ..constants.countries import COUNTRIES
from ..constants.country_operator import 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()

class DeleteApiKeyConfirmation(StatesGroup):
    waiting_for_confirmation = State()

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)

async def show_settings_menu(callback, state: FSMContext):
    user_id = callback.from_user.id if isinstance(callback, types.CallbackQuery) else callback.chat.id
    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)
    flag_emoji = COUNTRIES.get(user_country_code, "").split()[-1]

    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=f"{flag_emoji} Изменить страну", 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.message if isinstance(callback, types.CallbackQuery) else callback,
        new_text=message_text,
        state=state,
        reply_markup=keyboard.as_markup(),
        parse_mode="Markdown"
    )

    if not sent_message:
        try:
            sent_message = await callback.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)

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"
        "<blockquote expandable>"
        "<b>Получение вашего API ключа</b>\n"
        "Чтобы начать использовать API, вам нужно получить ваш уникальный API ключ. Следуйте этим шагам:\n"
        "1. Перейдите по <a href=\"tg://user?id=123456789\">ссылке на настройки SMSHUB</a>.\n"
        "2. Найдите кнопку \"Сгенерировать новый ключ\" и нажмите на неё.\n"
        "3. Ваша почта, на которую был зарегистрирован аккаунт, получит сообщение с вашим новым API ключом.\n"
        "4. Скопируйте ключ из письма.\n"
        "</blockquote>\n"
        "<blockquote expandable>"
        "<b>Ввод вашего API ключа</b>\n"
        "После получения ключа, вы можете ввести его в нашем боте. Для этого выполните следующие действия:\n"
        "1. Откройте интерфейс бота, выберите меню настроек.\n"
        "2. Нажмите на кнопку \"➕ Добавить API ключ\", чтобы начать процесс.\n"
        "3. Когда бот запросит, вставьте ваш API ключ в текстовое поле.\n"
        "4. Нажмите \"Отправить\".\n\n"
        "Ваш ключ будет проверен, и если он корректен, бот сохранит его для дальнейшего использования.\n"
        "</blockquote>\n"
        "<blockquote expandable>"
        "<b>Информация о безопасности</b>\n"
        "Не забывайте, что ваш API ключ должен оставаться конфиденциальным. Никому не сообщайте его, чтобы избежать несанкционированного доступа к вашим данным и услугам.\n"
        "</blockquote>"
    )

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text=prompt_text,
        state=state,
        reply_markup=keyboard.as_markup(),
        parse_mode="HTML",
        disable_web_page_preview=True
    )

    await state.set_state(UserSettings.entering_api_key)

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

    keyboard = InlineKeyboardBuilder()
    keyboard.button(text="✅ Да", callback_data="confirm_delete_api_key")
    keyboard.button(text="❌ Нет", callback_data="cancel_delete_api_key")
    keyboard.adjust(2)

    confirmation_text = "⚠️ Вы действительно хотите удалить ваш API ключ?"
    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text=confirmation_text,
        state=state,
        reply_markup=keyboard.as_markup(),
        parse_mode="Markdown"
    )

    await state.set_state(DeleteApiKeyConfirmation.waiting_for_confirmation)

async def confirm_delete_api_key_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")

    delete_user_api_key(callback_query.from_user.id)

    await show_settings_menu(callback_query, state)

async def cancel_delete_api_key_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)

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)

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)

            await show_settings_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()
            )
    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(),
        parse_mode="Markdown"
    )
    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)

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)
    await show_settings_menu(callback_query, state)

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(),
        parse_mode="Markdown"
    )
    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)

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)

    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 show_settings_menu(callback_query, state)

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(confirm_delete_api_key_handler, DeleteApiKeyConfirmation.waiting_for_confirmation, F.data == "confirm_delete_api_key")
    dp.callback_query.register(cancel_delete_api_key_handler, DeleteApiKeyConfirmation.waiting_for_confirmation, F.data == "cancel_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:"))
	
	
	
	
	
	
	
	
	
	
	# /bot/handlers/sms.py

import asyncio
import logging
import time
import json
from pathlib import Path
from typing import Optional, List, Tuple, Set
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.operators import OPERATOR_DISPLAY_NAMES
from ..constants.countries import COUNTRIES
from ..constants.services import 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() -> dict:
    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: dict):
    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(available_services: List[str]) -> List[Tuple[str, str]]:
    popularity_data = load_popularity_data()
    sorted_services = sorted(
        [(code, name) for code, name in SERVICES if code in available_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, services_pages: List[List[Tuple[str, str]]], services_with_numbers: Set[str]) -> InlineKeyboardBuilder:
    keyboard = InlineKeyboardBuilder()

    if page_number < len(services_pages):
        current_page = services_pages[page_number]

        for code, name in current_page:
            # Добавляем красный круг, если у сервиса нет доступных номеров
            if code not in services_with_numbers:
                name += " 🔴"
            keyboard.button(text=name, callback_data=f"service_select:{code}")

        keyboard.adjust(3)
        
        if len(services_pages) > 1:  # Показываем кнопки навигации только если страниц больше одной
            nav_buttons = []
            if page_number > 0:
                nav_buttons.append(types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"service_page:{page_number - 1}"))
            # Центрируем информацию о номере страницы
            nav_buttons.append(types.InlineKeyboardButton(text=f"Страница {page_number + 1}/{len(services_pages)}", callback_data="ignore"))
            if page_number < len(services_pages) - 1:
                nav_buttons.append(types.InlineKeyboardButton(text="➡️ Вперед", callback_data=f"service_page:{page_number + 1}"))
            keyboard.row(*nav_buttons)
        
        keyboard.row(types.InlineKeyboardButton(text="🔙 Назад в меню", callback_data="back_to_main_menu"))

    return keyboard

def get_service_message_text(total_services_with_numbers: int, total_services_for_country: int, services_with_operator: int, country_name: str, operator_name: str) -> str:
    return (
        f"🔢 *Общее кол-во сервисов:* `{total_services_for_country}`\n"
        f"💼 *Доступные сервисы (где есть номера):* `{total_services_with_numbers}`\n"
        f"✅ *Доступные сервисы (с вашими настройками оператора):* `{services_with_operator}`\n\n"
        f"— Нажмите на кнопку, чтобы выбрать сервис.\n"
        f"— Или введите название, чтобы найти сервис.\n\n"
        f"🐾 *Текущая страна:* `{country_name}`\n"
        f"📡 *Текущий оператор:* `{operator_name}`\n"
        f"_(Эти параметры можно изменить в настройках)_\n"
        f"\n_🔴 - нет номеров_"
    )

async def fetch_all_services(user_api_key: str, user_country_code: str, user_operator: str, state: FSMContext) -> Tuple[Optional[List[str]], Set[str], int, int]:
    smshub_api = SmsHubAPI(user_api_key)
    max_attempts = 3
    last_exception = None
    available_services = []
    services_with_numbers = set()

    for attempt in range(1, max_attempts + 1):
        try:
            # Получаем текущие данные из состояния
            data = await state.get_data()
            message = data.get('message')

            # Отправляем прогресс-сообщение
            progress_text = f"Идет запрос списка сервисов {attempt}/3..."
            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()
            )

            # Выполняем запрос к API с указанным оператором
            numbers_status = await smshub_api.get_numbers_status(country=user_country_code, operator=user_operator)
            if numbers_status:
                # Все сервисы и сервисы с доступными номерами
                available_services = [service.split('_')[0] for service in numbers_status.keys()]
                services_with_numbers = {service.split('_')[0] for service, count in numbers_status.items() if int(count) > 0}
                break
        except Exception as e:
            last_exception = e
            logger.error(f"Ошибка при запросе статуса всех сервисов, попытка {attempt}: {e}")

        data = await state.get_data()
        if data.get("cancel_request"):
            logger.info("Запрос на получение всех сервисов был отменен.")
            return None, set(), 0, 0

        await asyncio.sleep(5)

    if last_exception:
        logger.error(f"Ошибка при запросе всех сервисов: {last_exception}")
        return None, set(), 0, 0

    return available_services, services_with_numbers, len(services_with_numbers), len(available_services)

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)
    user_country_code = get_user_country(callback_query.from_user.id) or "0"
    user_operator = get_user_operator(callback_query.from_user.id) or "any"

    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} начал процесс получения номера.")

    await state.update_data(message=callback_query.message, cancel_request=False)
    available_services, services_with_numbers, total_services_with_numbers, total_services_for_country = await fetch_all_services(
        user_api_key, user_country_code, user_operator, state
    )

    country_name = COUNTRIES.get(user_country_code, 'Неизвестно')
    operator_name = OPERATOR_DISPLAY_NAMES.get(user_operator, 'Любой')

    await state.update_data(
        total_services_with_numbers=total_services_with_numbers, 
        total_services_for_country=total_services_for_country,
        country_name=country_name, 
        operator_name=operator_name
    )

    if available_services is None:
        await send_main_menu(callback_query.message, state)
        return

    services_with_operator = services_with_numbers  # Теперь это множество

    # Сортируем и разделяем на страницы
    sorted_services = get_sorted_services(available_services)
    services_pages = get_service_pages(sorted_services)

    keyboard = build_service_keyboard(page_number=0, services_pages=services_pages, services_with_numbers=services_with_operator)
    message_text = get_service_message_text(
        total_services_with_numbers, 
        total_services_for_country, 
        len(services_with_operator), 
        country_name, 
        operator_name
    )

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text=message_text,
        state=state,
        reply_markup=keyboard.as_markup(),
        parse_mode="Markdown"
    )
    await state.set_state(SmsOrderState.waiting_for_service)
    await state.update_data(
        available_services=available_services,
        services_pages=services_pages,  # Сохраняем страницы для навигации
        services_with_numbers=services_with_operator
    )

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()

    available_services = data.get('available_services', [])
    total_services_with_numbers = data.get('total_services_with_numbers', len(available_services))
    total_services_for_country = data.get('total_services_for_country', len(available_services))
    country_name = data.get('country_name', 'Неизвестно')
    operator_name = data.get('operator_name', 'Любой')
    services_pages = data.get('services_pages', [])
    services_with_numbers = data.get('services_with_numbers', set())

    # Оставляем сервисы неизменными, не делаем повторный запрос к API
    keyboard = build_service_keyboard(page_number=0, services_pages=services_pages, services_with_numbers=services_with_numbers)
    message_text = get_service_message_text(
        total_services_with_numbers, 
        total_services_for_country, 
        len(services_with_numbers), 
        country_name, 
        operator_name
    )

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text=message_text,
        state=state,
        reply_markup=keyboard.as_markup(),
        parse_mode="Markdown"
    )

    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 state.set_state(None)
    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])
    data = await state.get_data()
    services_pages = data.get('services_pages', [])
    services_with_numbers = data.get('services_with_numbers', set())
    total_services_with_numbers = data.get('total_services_with_numbers', 0)
    total_services_for_country = data.get('total_services_for_country', 0)
    country_name = data.get('country_name', 'Неизвестно')
    operator_name = data.get('operator_name', 'Любой')

    if page_number < 0 or page_number >= len(services_pages):
        # Некорректный номер страницы, возвращаемся к первой странице
        page_number = 0

    keyboard = build_service_keyboard(page_number, services_pages, services_with_numbers)
    message_text = get_service_message_text(
        total_services_with_numbers, 
        total_services_for_country, 
        len(services_with_numbers), 
        country_name, 
        operator_name
    )

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text=message_text,
        state=state,
        reply_markup=keyboard.as_markup(),
        parse_mode="Markdown"
    )

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

    data = await state.get_data()
    available_services = data.get('available_services', [])
    services_with_numbers = data.get('services_with_numbers', set())

    matched_services = [
        (code, name) for code, name in SERVICE_DISPLAY_NAMES.items()
        if code in available_services and (user_input in name.lower() or user_input == code.lower())
    ]

    if matched_services:
        pages = get_service_pages(matched_services)
        page_number = 0
        keyboard = build_service_keyboard(pages, page_number, services_with_numbers)
        
        await state.update_data(matched_services=matched_services, services_pages=pages)

        await safe_edit_or_send_message(
            message=message,
            new_text="Пожалуйста, выберите один из представленных сервисов:\n_🔴 - нет номеров_",
            state=state,
            reply_markup=keyboard.as_markup(),
            parse_mode="Markdown"
        )
    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()
        )

def build_page_keyboard(service_pages: List[List[Tuple[str, str]]], page_number: int, services_with_numbers: Set[str]) -> InlineKeyboardBuilder:
    keyboard = InlineKeyboardBuilder()
    if page_number < len(service_pages):
        current_page = service_pages[page_number]

        for code, name in current_page:
            # Добавляем красный круг, если у сервиса нет доступных номеров
            if code not in services_with_numbers:
                name += " 🔴"
            keyboard.button(text=name, callback_data=f"service_select:{code}")

        keyboard.adjust(3)
        
        if len(service_pages) > 1:  # Показываем кнопки навигации только если страниц больше одной
            nav_buttons = []
            if page_number > 0:
                nav_buttons.append(types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"search_page:{page_number - 1}"))
            # Центрируем информацию о номере страницы
            nav_buttons.append(types.InlineKeyboardButton(text=f"Страница {page_number + 1}/{len(service_pages)}", callback_data="ignore"))
            if page_number < len(service_pages) - 1:
                nav_buttons.append(types.InlineKeyboardButton(text="➡️ Вперед", callback_data=f"search_page:{page_number + 1}"))
            keyboard.row(*nav_buttons)
    
    keyboard.row(types.InlineKeyboardButton(text="🔙 Назад к сервисам", callback_data="service_page:0"))
    return keyboard

async def handle_search_page_navigation(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])
    data = await state.get_data()
    matched_services = data.get('matched_services', [])
    services_pages = data.get('services_pages', [])
    services_with_numbers = data.get('services_with_numbers', set())
    total_services_with_numbers = data.get('total_services_with_numbers', 0)
    total_services_for_country = data.get('total_services_for_country', 0)
    country_name = data.get('country_name', 'Неизвестно')
    operator_name = data.get('operator_name', 'Любой')

    if page_number < 0 or page_number >= len(services_pages):
        # Некорректный номер страницы, возвращаемся к первой странице
        page_number = 0

    keyboard = build_service_keyboard(page_number, services_pages, services_with_numbers)
    message_text = get_service_message_text(
        total_services_with_numbers, 
        total_services_for_country, 
        len(services_with_numbers), 
        country_name, 
        operator_name
    )

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text=message_text,
        state=state,
        reply_markup=keyboard.as_markup(),
        parse_mode="Markdown"
    )

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

    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}/3..."
            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 data.get("cancel_request"):
                logger.info(f"Запрос для пользователя {user_id} был отменен после получения данных.")
                return

            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:
                    prices = [float(price_str) for price_str in price_info.keys() if price_str is not None]
                    if prices:
                        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
                    else:
                        logger.info(f"Попытка {attempt}: Цены не найдены.")
                        last_exception = "Цены не найдены"
                else:
                    logger.info(f"Попытка {attempt}: Нет доступных номеров.")
                    last_exception = "Нет доступных номеров"

        except Exception as e:
            logger.error(f"Ошибка при получении номера для сервиса '{service_code}', попытка {attempt}: {e}")
            last_exception = e

        await asyncio.sleep(5)

    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', 0)
        max_price_usd = user_data.get('max_price_usd', 0)
        min_price_rub = user_data.get('min_price_rub', 0)
        max_price_rub = user_data.get('max_price_rub', 0)

        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', 0):
        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', 0):.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}/3..."
            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"

            if data.get("cancel_request"):
                logger.info(f"Запрос на получение номера для пользователя {user_id} был отменен перед выполнением.")
                return

            result = await smshub_api.get_number(service, country=user_country_code, max_price=max_price, operator=user_operator)

            if data.get("cancel_request"):
                logger.info(f"Запрос на получение номера для пользователя {user_id} был отменен.")
                return

            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"Вы можете управлять активацией или вернуться в главное меню."
                )

                sent_message = await safe_edit_or_send_message(
                    message=message,
                    new_text=message_text,
                    state=state,
                    reply_markup=keyboard.as_markup(),
                    parse_mode="Markdown"
                )
                user_activations[user_id][activation_id]['message_id'] = sent_message.message_id

                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()
                    )
                    return
            else:
                raise Exception("Неожиданный ответ API")

        except Exception as e:
            logger.error(f"Ошибка при получении номера для сервиса '{service_code}', попытка {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()
        )

# Добавляем обработчик для колбэка "ignore" чтобы игнорировать его
async def ignore_callback_handler(callback_query: types.CallbackQuery):
    try:
        await callback_query.answer()  # Просто отвечаем, чтобы подтвердить действие
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")

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(handle_search_page_navigation, F.data.startswith("search_page:"))
    dp.callback_query.register(price_selected_handler, F.data.startswith("select_price:"))
    dp.callback_query.register(ignore_callback_handler, F.data == "ignore")
    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'))