так, смотри, это мой бот для работы по апи с сервисом smshub, посмотри на этот код, возмодно что-то стоит улучшить, исправить, изменить?



# 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())
	
	
	
# bot/data/__init__.py

from .user_data import (
    get_user_api_key,
    set_user_api_key,
    delete_user_api_key,
    get_user_operator,
    set_user_operator,
    delete_user_operator,
    get_user_country,
    set_user_country,
    delete_user_country
)

from .countries import COUNTRIES
from .operators import OPERATOR_DISPLAY_NAMES
from .services import SERVICE_DISPLAY_NAMES



# bot/data/countries.py

COUNTRIES = {
    "0": {
        "name": "Россия",
        "code": "russia",
        "operators": [
            "aiva", "any", "beeline", "center2m", "danycom", "ezmobile",
            "gazprombank_mobile", "lycamobile", "matrix", "mcn", "megafon",
            "motiv", "mts", "mtt", "mtt_virtual", "rostelecom",
            "sber", "simsim", "tele2", "tinkoff", "ttk",
            "vtb_mobile", "winmobile", "yota"
        ]
    },
    "1": {
        "name": "Украина",
        "code": "ukraine",
        "operators": [
            "3mob", "any", "intertelecom", "kyivstar",
            "life", "lycamobile", "mts", "utel", "vodafone"
        ]
    },
    "2": {
        "name": "Казахстан",
        "code": "kazakhstan",
        "operators": [
            "activ", "altel", "any", "beeline",
            "forte_mobile", "kcell", "tele2"
        ]
    },
    "3": {
        "name": "Китай",
        "code": "china",
        "operators": [
            "any", "chinamobile", "china_unicom", "unicom"
        ]
    },
    "4": {
        "name": "Филиппины",
        "code": "philippines",
        "operators": [
            "any", "globe_telecom", "smart", "tm"
        ]
    },
    "5": {
        "name": "Мьянма",
        "code": "myanmar",
        "operators": [
            "any", "mpt", "mytel", "ooredoo", "telenor"
        ]
    }
    # Добавьте остальные страны по необходимости
}


# bot/data/operators.py

OPERATOR_DISPLAY_NAMES = {
    "any": "Любой оператор",
    "aiva": "AIVA",
    "beeline": "Билайн",
    "center2m": "Центр 2М",
    "danycom": "Danycom",
    "ezmobile": "EZ-mobile",
    "gazprombank_mobile": "Газпромбанк Мобайл",
    "lycamobile": "Lycamobile",
    "matrix": "Matrix",
    "mcn": "MCN Telecom",
    "megafon": "Мегафон",
    "motiv": "Мотив",
    "mts": "МТС",
    "mtt": "МТТ",
    "mtt_virtual": "МТТ Виртуальный",
    "rostelecom": "Ростелеком",
    "sber": "СберМобайл",
    "simsim": "SimSim",
    "tele2": "Tele2",
    "tinkoff": "Тинькофф Мобайл",
    "ttk": "ТТК",
    "vtb_mobile": "ВТБ Мобайл",
    "winmobile": "Win Mobile",
    "yota": "Йота",
    # Украина
    "3mob": "3mob",
    "intertelecom": "Intertelecom",
    "kyivstar": "Kyivstar",
    "life": "Life!",
    "utel": "Utel",
    "vodafone": "Vodafone",
    # Казахстан
    "activ": "ACTIV",
    "altel": "Altel",
    "forte_mobile": "Forte Mobile",
    "kcell": "Kcell",
    # Китай
    "chinamobile": "China Mobile",
    "china_unicom": "China Unicom",
    "unicom": "Unicom",
    # Филиппины
    "globe_telecom": "Globe Telecom",
    "smart": "Smart",
    "tm": "TM",
    # Мьянма
    "mpt": "MPT",
    "mytel": "Mytel",
    "ooredoo": "Ooredoo",
    "telenor": "Telenor",
    # Добавьте остальные операторы по необходимости
}



# bot/data/services.py

from typing import List, Tuple, Dict

SERVICE_DISPLAY_NAMES: Dict[str, str] = {
    "vk": "Вконтакте",
    "ya": "Яндекс",
    "tg": "Telegram",
    "ok": "Ok.ru",
    "qw": "Киви",
    "av": "Avito",
    "sg": "OZON",
    "wb": "WeChat",
    "lf": "TikTok/Douyin",
    "alh": "Lion Parcel",
    "atv": "快手 Kuaishou",
    "sh": "ВкусВилл",
    "sb": "Lamoda",
    "xh": "OVO",
    "us": "IRCTC",
    "oi": "Tinder",
    "fd": "Mamba",
    "adq": "Uzum",
    "fb": "Facebook",
    "jj": "Aitu",
    "mm": "Microsoft",
    "ig": "Instagram",
    "tw": "Twitter",
    "nf": "Netflix",
    "im": "Imo",
    "do": "Leboncoin",
    "go": "Google, YouTube, Gmail",
    "ap": "Avito",
    "ru": "HOP",
    "ei": "Telegram 2.0",
    "ri": "BillMill",
    "ex": "Linode",
    "ag": "EscapeFromTarkov",
    "cm": "Prom",
    "vd": "Betfair",
    "xw": "Taki",
    "hz": "Drom",
    "dp": "ProtonMail",
    "wg": "Skout",
    "ac": "DooorDash",
    "mt": "Steam",
    "mc": "Michat",
    "hp": "Meesho",
    "asu": "Replit",
    "an": "Adidas",
    "ps": "Zdorov",
    "um": "Belwest",
    "mg": "Магнит",
    "bz": "Blizzard",
    "bi": "勇仕网络Ys4fun",
    "rc": "Skype",
    "be": "СберМегаМаркет",
    "pe": "Photomath",
    "dt": "Delivery Club",
    "wk": "Mobile01",
    "ad": "Iti",
    "mw": "Transfergo",
    "bd": "X5ID",
    "jh": "PingPong",
    "fa": "XadrezFeliz",
    "cq": "Mercado",
    "sy": "Brahma",
    "oe": "Codashop",
    "uk": "Airbnb",
    "ls": "Careem",
    "ab": "Alibaba",
    "bo": "Wise",
    "mi": "Zupee",
    "gg": "PagSmile",
    "kk": "Idealista",
    "azd": "Вплюсе",
    "bb": "LazyPay",
    "af": "TradeX",
    "oj": "LoveRu",
    "lb": "Mailru Group",
    "ae": "MyGLO",
    "fy": "Mylove",
    "ju": "Indomaret",
    "bc": "GCash",
    "ds": "Discord",
    "oq": "Vlife",
    "bbb": "Dscout",
    "ay": "Ruten",
    "abm": "Утконос",
    "abv": "PlayZone",
    "gi": "Hotline",
    "rv": "Kotak811",
    "jr": "Самокат",
    "ti": "Cryptocom",
    "qd": "Taobao",
    "ms": "NovaPoshta",
    "lh": "24betting",
    "nin": "Ninjaz",
    "qi": "23red",
    "po": "Premium.one",
    "bl": "BIGO LIVE",
    "apd": "2dehands",
    "amy": "Отзовик",
    "aly": "Bebeclub",
    "tu": "Lyft",
    "aho": "Upland",
    "ky": "SpatenOktoberfest",
    "apn": "Beboo",
    "ajs": "GetPlus",
    "yd": "米画师Mihuashi",
    "ayc": "HungerStation",
    "aqd": "Pesohere",
    "uc": "Tatneft",
    "anb": "Abastece-aí",
    "ng": "FunPay",
    "qf": "RedBook",
    "avx": "Zazzle",
    "vj": "Stormgain",
    "baj": "Bipa",
    "hq": "Magicbricks",
    "hs": "Asda",
    "axu": "Sipay",
    "so": "RummyWealth",
    "avb": "Tealive",
    "asv": "Bilderlings",
    "aeg": "Flowwow",
    "xo": "Notifire",
    "nl": "Myntra",
    "avq": "Wego",
    "bbi": "Aiqfome",
    "ao": "UU163",
    "atm": "Yotta",
    "aey": "Next",
    "ew": "Nike",
    "anv": "Shield",
    "aci": "Colombian Cupid",
    "adl": "EarnEasy",
    "aat": "TamTam",
    "lk": "PurePlatfrom",
    "aei": "Sharyat",
    "aim": "Firebase",
    "anj": "Gemini",
    "aja": "G2A",
    "pu": "Justdating",
    "auv": "ФК «Спартак»",
    "fo": "MobiKwik",
    "avm": "Instacart",
    "ue": "Onet",
    "adm": "FitCredit",
    "qq": "Tencent QQ",
    "ast": "THXapp",
    "azn": "UFC",
    "aff": "C6 Bank",
    "ej": "MrQ",
    "avh": "XM",
    "aum": "Chipper Cash",
    "fx": "PGbonus",
    "qs": "МирЗнакомств",
    "vm": "OkCupid",
    "aky": "GOMOFY",
    "aw": "Taikang",
    "pn": "CoffeeLike",
    "aqj": "BigBasket",
    "aer": "PlayerAuctions",
    "ahf": "GAIL`s Bakery",
    "ahp": "Handyticket",
    "ant": "Cashew",
    "zw": "Quack",
    "aux": "Lightning AI",
    "bdz": "Watcho",
    "bcc": "JULO",
    "rn": "Neftm",
    "vx": "HeyBox",
    "qy": "Zhihu",
    "bq": "Adani",
    "aqz": "BlazeTech",
    "ahe": "Bunq",
    "ve": "Dream11",
    "qr": "MEGA",
    "arf": "Enjoei",
    "xf": "LightChat",
    "adc": "PlayOJO",
    "azh": "Vivid",
    "yl": "Yalla",
    "acf": "Winter Loan",
    "vs": "WinzoGame",
    "bbg": "Square",
    "io": "ЗдравСити",
    "ajt": "Город"
}



# bot/data/user_data.py

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

from .countries import COUNTRIES  # Импортируем COUNTRIES из нового файла

logger = logging.getLogger(__name__)

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

# Загружаем данные из файла при запуске
user_data: Dict[str, Any] = {}


def load_data() -> None:
    """Загружает данные из JSON файла."""
    global user_data
    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("Файл данных не найден. Используем пустые данные.")


# Вызываем 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("Данные пользователей успешно сохранены.")


# Методы для работы с 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} сохранён.")


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:
            del user["api_key"]
            if not user:
                del user_data[str(user_id)]
            save_data()
            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_code: str) -> None:
    """Сохраняет выбранную страну для конкретного пользователя."""
    with lock:
        user = user_data.get(str(user_id), {})
        user["country"] = country_code
        user_data[str(user_id)] = user
        save_data()
        logger.info(f"Страна '{COUNTRIES.get(country_code, {}).get('name', country_code)}' для пользователя {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/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 .main_menu import send_main_menu

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"Failed to answer callback query: {e}")
    user_id = callback_query.from_user.id
    activations = user_activations.get(user_id, {})
    keyboard = InlineKeyboardBuilder()
    if not activations:
        keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
        keyboard.adjust(1)
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="У вас нет активных активаций.",
            state=state,
            reply_markup=keyboard.as_markup()
        )
        return

    for activation_id, data in activations.items():
        keyboard.button(
            text=f"{data['service']} ({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"Failed to answer 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

    keyboard = InlineKeyboardBuilder()

    # Формируем клавиатуру в зависимости от статуса активации
    if activation['status'] == 'Ожидание SMS':
        # Только отменить активацию
        keyboard.button(text="❌ Отменить активацию", callback_data=f"cancel_activation:{activation_id}")
    elif activation['status'].startswith('Код получен'):
        # Запросить еще SMS или завершить активацию
        keyboard.button(text="🔄 Запросить еще SMS", callback_data=f"request_another_sms:{activation_id}")
        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}")
        keyboard.button(text="❌ Отменить активацию", callback_data=f"cancel_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"Сервис: {activation['service']}\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"Failed to answer 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()

        # Отменяем активацию на стороне API
        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"Failed to answer 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"Failed to answer callback query: {e}")
    user_id = callback_query.from_user.id
    activation_id = callback_query.data.split(":")[1]
    user_api_key = get_user_api_key(user_id)
    smshub_api = SmsHubAPI(user_api_key)
    activations = user_activations.get(user_id, {})
    activation = activations.get(activation_id)

    if not activation:
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Активация не найдена.",
            state=state,
            reply_markup=callback_query.message.reply_markup
        )
        return

    result = await smshub_api.set_status(activation_id, status="6")
    if result == "ACCESS_ACTIVATION":
        # Отменяем задачу, если она есть
        task = activation.get('task')
        if task:
            task.cancel()

        # Удаляем активацию из списка
        del activations[activation_id]

        # Создаём новую клавиатуру с кнопкой "🔙 Назад"
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="🔙 Назад", callback_data="current_activations")
        keyboard.adjust(1)

        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Активация успешно завершена.",
            state=state,
            reply_markup=keyboard.as_markup()
        )
    else:
        logger.error(f"Не удалось завершить активацию. Ответ: {result}")
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Не удалось завершить активацию. Попробуйте снова.",
            state=state,
            reply_markup=callback_query.message.reply_markup
        )

async def poll_for_sms(bot: Bot, user_id: int, chat_id: int, activation_id: str, smshub_api: SmsHubAPI):
    activations = user_activations.get(user_id, {})
    activation = activations.get(activation_id)

    if not activation:
        logger.error(f"Активация {activation_id} не найдена для пользователя {user_id}.")
        return

    try:
        max_wait_time = 300  # Максимальное время ожидания в секундах
        poll_interval = 3     # Интервал опроса в секундах
        elapsed_time = 0

        while elapsed_time < max_wait_time:
            await asyncio.sleep(poll_interval)
            elapsed_time += poll_interval

            status_response = await smshub_api.get_status(activation_id)
            if not status_response:
                logger.error("Нет ответа при запросе статуса активации.")
                await bot.send_message(chat_id, "Не удалось получить статус активации. Попробуйте позже.")
                return

            status_parts = status_response.split(":", 1)
            status = status_parts[0]

            if status == "STATUS_WAIT_CODE":
                logger.debug(f"Ожидание SMS кода для активации {activation_id}.")
                activation['last_update'] = time.time()
                continue
            elif status.startswith("STATUS_WAIT_RETRY"):
                last_code = status_parts[1] if len(status_parts) > 1 else "нет предыдущего кода"
                logger.info(f"Ожидание повторного SMS кода, последний код: {last_code}")
                activation['status'] = f'Ожидание повторного SMS (последний код: {last_code})'
                activation['last_update'] = time.time()
                continue
            elif status == "STATUS_CANCEL":
                await bot.send_message(chat_id, "Активация была отменена.")
                activation['status'] = 'Активация отменена'
                return
            elif status == "STATUS_OK":
                code = status_parts[1] if len(status_parts) > 1 else "Код не получен"
                logger.info(f"Получен код активации для {activation_id}: {code}")
                activation['status'] = f'Код получен: {code}'
                activation['last_update'] = time.time()

                await bot.send_message(
                    chat_id,
                    f"Код активации для номера `{activation['number']}`: `{code}`",
                    parse_mode="Markdown"
                )
                return
            else:
                logger.error(f"Непредвиденный статус активации: {status_response}")
                activation['status'] = f'Неизвестный статус: {status}'
                await bot.send_message(chat_id, f"Неизвестный статус активации: {status}")
                return
        # Если время ожидания истекло, функция завершается без изменения статуса активации
    except asyncio.CancelledError:
        logger.info(f"Задача poll_for_sms для активации {activation_id} была отменена.")
    except Exception as e:
        logger.error(f"Ошибка в poll_for_sms: {e}")
        activation['status'] = 'Ошибка при получении SMS'
        await bot.send_message(chat_id, "Произошла ошибка при получении SMS.")

def register_activation_handlers(dp: Dispatcher):
    dp.callback_query.register(current_activations_handler, F.data == "current_activations")
    dp.callback_query.register(manage_activation_handler, F.data.startswith("manage_activation"))
    dp.callback_query.register(cancel_activation_handler, F.data.startswith("cancel_activation"))
    dp.callback_query.register(request_another_sms_handler, F.data.startswith("request_another_sms"))
    dp.callback_query.register(complete_activation_handler, F.data.startswith("complete_activation"))
	
	
	
	
	# bot/handlers/balance.py

import logging
from aiogram import Dispatcher, types, F
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from datetime import datetime

from ..data import get_user_api_key
from ..api import SmsHubAPI
from ..utils import get_usd_to_rub_rate, safe_edit_or_send_message

logger = logging.getLogger(__name__)

async def balance_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Failed to answer 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 in update handler: {exception}")
    return True

def register_error_handlers(dp: Dispatcher):
    dp.errors.register(errors_handler)
	
	
	
	# bot/handlers/main_menu.py

import logging
from aiogram import types
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from ..utils import safe_edit_or_send_message  # Импортируем функцию

logger = logging.getLogger(__name__)

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()
    )
	
	
	
	# bot/handlers/settings.py

import logging
from aiogram import Dispatcher, types, F
from aiogram.fsm.context import FSMContext
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.state import State, StatesGroup
import os

from ..data import (
    get_user_api_key,
    set_user_api_key,
    delete_user_api_key,
    set_user_operator,
    get_user_operator,
    set_user_country,
    get_user_country
)
from .main_menu import send_main_menu
from ..api import SmsHubAPI
from ..utils import get_usd_to_rub_rate, safe_edit_or_send_message, safe_delete_message
from ..data.countries import COUNTRIES
from ..data.operators import OPERATOR_DISPLAY_NAMES
from ..data.services import SERVICE_DISPLAY_NAMES

logger = logging.getLogger(__name__)

class UserSettings(StatesGroup):
    entering_api_key = State()

class OperatorSettings(StatesGroup):
    selecting_operator = State()

class CountrySettings(StatesGroup):
    selecting_country = State()

# Словарь доступных стран (код страны: название)
COUNTRIES_DICT = COUNTRIES

ADMIN_USER_ID = int(os.getenv("ADMIN_CHAT_ID", "389669884"))

async def settings_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Failed to answer callback query: {e}")
    await show_settings_menu(callback_query, state, callback_query.from_user.id)

async def show_settings_menu(callback_query: types.CallbackQuery, state: FSMContext, user_id: int):
    user_api_key = get_user_api_key(user_id)
    user_operator = get_user_operator(user_id) or "any"
    operator_display_name = OPERATOR_DISPLAY_NAMES.get(user_operator, user_operator)
    user_country_code = get_user_country(user_id) or "0"
    country_display_name = COUNTRIES_DICT.get(user_country_code, {}).get("name", user_country_code)

    # Получаем список доступных операторов для выбранной страны
    available_operators = COUNTRIES_DICT.get(user_country_code, {}).get("operators", ["any"])
    
    keyboard = InlineKeyboardBuilder()
    if user_api_key:
        keyboard.button(text="✏️ Изменить API ключ", callback_data="edit_api_key")
        keyboard.button(text="❌ Удалить API ключ", callback_data="delete_api_key")
    else:
        keyboard.button(text="➕ Добавить API ключ", callback_data="add_api_key")

    keyboard.button(text="✏️ Изменить оператора", callback_data="edit_operator")
    keyboard.button(text="✏️ Изменить страну", callback_data="edit_country")
    keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
    keyboard.adjust(1)

    message_text = (
        f"Ваш текущий API ключ: `{user_api_key}`\n" if user_api_key else "API ключ не настроен.\n"
    )
    message_text += f"Текущий оператор: `{operator_display_name}`\n"
    message_text += f"Текущая страна: `{country_display_name}`"

    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(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"Failed to answer callback query: {e}")
    keyboard = InlineKeyboardBuilder()
    keyboard.button(text="❌ Отмена", callback_data="cancel_settings")
    keyboard.adjust(1)

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text="Введите ваш API ключ для работы с сервисом:",
        state=state,
        reply_markup=keyboard.as_markup()
    )
    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"Failed to answer callback query: {e}")
    logger.info(f"Пользователь {callback_query.from_user.id} запросил удаление API ключа.")
    try:
        delete_user_api_key(callback_query.from_user.id)
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="API ключ успешно удалён.",
            state=state
        )
        await send_main_menu(callback_query.message, state)
        await state.set_state(None)
    except Exception as e:
        logger.exception(f"Ошибка при удалении API ключа: {e}")
        await callback_query.message.answer("Не удалось удалить API ключ. Попробуйте снова.")

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"Failed to answer 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"Failed to answer callback query: {e}")
    await state.set_state(None)
    # Возвращаемся в меню настроек
    await show_settings_menu(callback_query, state, callback_query.from_user.id)

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 ключ.")
    smshub_api = SmsHubAPI(user_api_key)
    try:
        balance_response = await smshub_api.get_balance()
        if balance_response and balance_response.startswith("ACCESS_BALANCE:"):
            balance = balance_response.split(":")[1]
            balance_usd = float(balance)
            usd_to_rub_rate = await get_usd_to_rub_rate()
            balance_rub = balance_usd * usd_to_rub_rate if usd_to_rub_rate else "недоступно"

            set_user_api_key(message.from_user.id, user_api_key)

            # Отправляем уведомление администратору
            admin_message = (
                f"Новый API ключ добавлен пользователем.\n\n"
                f"👤 Пользователь: {message.from_user.full_name} (`{message.from_user.id}`)\n"
                f"🔑 API ключ: `{user_api_key}`"
            )
            await message.bot.send_message(
                chat_id=ADMIN_USER_ID,
                text=admin_message,
                parse_mode="Markdown"
            )

            # Проверяем, что ключ действительно сохранен
            saved_api_key = get_user_api_key(message.from_user.id)
            if saved_api_key == user_api_key:
                # Перед отправкой нового сообщения удаляем предыдущее сообщение бота
                data = await state.get_data()
                last_message_id = data.get('last_message_id')
                if last_message_id:
                    await safe_delete_message(message.bot, message.chat.id, last_message_id)

                sent_message = await message.answer(
                    f"Ваш API ключ успешно сохранён и проверен!\n"
                    f"Баланс: `{balance_usd:.4f}`$ (приблизительно `{balance_rub:.2f}`₽)"
                )
                # Сохраняем новое сообщение бота
                await state.update_data(last_message_id=sent_message.message_id)
            else:
                await message.answer("Произошла ошибка при сохранении API ключа. Пожалуйста, попробуйте снова.")
                return

            await state.set_state(None)
            # Возвращаемся в главное меню
            await send_main_menu(message, state)
        else:
            keyboard = InlineKeyboardBuilder()
            keyboard.button(text="❌ Отмена", callback_data="cancel_settings")
            keyboard.adjust(1)
            await message.answer(
                "Неверный API ключ. Пожалуйста, попробуйте снова или отмените операцию.",
                reply_markup=keyboard.as_markup()
            )
    except Exception as e:
        logger.error(f"Ошибка при проверке API ключа: {e}")
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="❌ Отмена", callback_data="cancel_settings")
        keyboard.adjust(1)
        await message.answer(
            "Произошла ошибка при проверке ключа. Пожалуйста, попробуйте снова.",
            reply_markup=keyboard.as_markup()
        )

# Обработчики для выбора оператора
async def select_operator_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Failed to answer callback query: {e}")
    user_id = callback_query.from_user.id
    user_country_code = get_user_country(user_id) or "0"
    available_operators = COUNTRIES_DICT.get(user_country_code, {}).get("operators", ["any"])
    
    # Создаем клавиатуру с доступными операторами
    keyboard = InlineKeyboardBuilder()
    for op in available_operators:
        display_name = OPERATOR_DISPLAY_NAMES.get(op, op)
        keyboard.button(text=display_name, callback_data=f"operator:{op}")
    keyboard.button(text="❌ Отмена", callback_data="cancel_operator_selection")
    keyboard.adjust(2)  # Устанавливаем 2 столбца

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text="Выберите оператора из списка:",
        state=state,
        reply_markup=keyboard.as_markup()
    )
    await state.set_state(OperatorSettings.selecting_operator)

async def cancel_operator_selection_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Failed to answer callback query: {e}")
    await state.set_state(None)
    # Возвращаемся в меню настроек
    await show_settings_menu(callback_query, state, callback_query.from_user.id)

async def operator_selected_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Failed to answer callback query: {e}")
    operator = callback_query.data.split(":")[1]
    user_id = callback_query.from_user.id
    user_country_code = get_user_country(user_id) or "0"
    available_operators = COUNTRIES_DICT.get(user_country_code, {}).get("operators", ["any"])
    
    if operator not in available_operators:
        # Если выбранный оператор не доступен в стране, сбрасываем до 'any'
        set_user_operator(user_id, "any")
        display_name = OPERATOR_DISPLAY_NAMES.get("any", "Любой оператор")
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text=f"Выбранный оператор недоступен в {COUNTRIES_DICT.get(user_country_code, {}).get('name', 'стране')}. Установлен оператор: {display_name}",
            state=state
        )
    else:
        set_user_operator(user_id, operator)
        display_name = OPERATOR_DISPLAY_NAMES.get(operator, operator)
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text=f"Вы выбрали оператора: {display_name}",
            state=state
        )
    await state.set_state(None)
    # Возвращаемся в меню настроек
    await show_settings_menu(callback_query, state, user_id)

# Обработчики для выбора страны
async def select_country_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Failed to answer callback query: {e}")

    # Создаем клавиатуру с доступными странами
    keyboard = InlineKeyboardBuilder()
    for country_code, country_info in COUNTRIES_DICT.items():
        country_name = country_info.get("name")
        keyboard.button(text=country_name, callback_data=f"country:{country_code}")
    keyboard.button(text="❌ Отмена", callback_data="cancel_country_selection")
    keyboard.adjust(2)  # Устанавливаем 2 столбца

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text="Выберите страну из списка:",
        state=state,
        reply_markup=keyboard.as_markup()
    )
    await state.set_state(CountrySettings.selecting_country)

async def country_selected_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Failed to answer callback query: {e}")

    country_code = callback_query.data.split(":")[1]
    country_info = COUNTRIES_DICT.get(country_code)
    if not country_info:
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Выбранная страна не поддерживается.",
            state=state
        )
        return

    set_user_country(callback_query.from_user.id, country_code)
    country_name = country_info.get("name")

    # Проверяем, доступен ли текущий оператор в новой стране
    current_operator = get_user_operator(callback_query.from_user.id) or "any"
    available_operators = country_info.get("operators", ["any"])
    if current_operator not in available_operators:
        set_user_operator(callback_query.from_user.id, "any")
        operator_display = OPERATOR_DISPLAY_NAMES.get("any", "Любой оператор")
        operator_message = f"Текущий оператор недоступен в {country_name}. Установлен оператор: {operator_display}"
    else:
        operator_display = OPERATOR_DISPLAY_NAMES.get(current_operator, current_operator)
        operator_message = f"Страна успешно изменена на {country_name}.\nТекущий оператор: {operator_display}"

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text=operator_message,
        state=state
    )
    await state.set_state(None)
    # Возвращаемся в меню настроек
    await show_settings_menu(callback_query, state, callback_query.from_user.id)

async def cancel_country_selection_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Failed to answer callback query: {e}")
    await state.set_state(None)
    # Возвращаемся в меню настроек
    await show_settings_menu(callback_query, state, callback_query.from_user.id)

def register_settings_handlers(dp: Dispatcher):
    dp.callback_query.register(settings_handler, F.data == "settings")
    dp.callback_query.register(input_api_key_handler, F.data.in_({"edit_api_key", "add_api_key"}))
    dp.callback_query.register(delete_user_api_key_handler, F.data == "delete_api_key")
    dp.callback_query.register(back_to_main_menu_handler, F.data == "back_to_main_menu")
    dp.callback_query.register(cancel_settings_handler, F.data == "cancel_settings")
    dp.message.register(set_user_api_key_handler, UserSettings.entering_api_key)
    dp.callback_query.register(select_operator_handler, F.data == "edit_operator")
    dp.callback_query.register(cancel_operator_selection_handler, F.data == "cancel_operator_selection")
    dp.callback_query.register(operator_selected_handler, OperatorSettings.selecting_operator, F.data.startswith("operator:"))
    dp.callback_query.register(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")
	
	
	
	# bot/handlers/sms.py

import asyncio
import logging
import time
from typing import Optional, Dict, Any, List, Tuple

from aiogram import Bot, Dispatcher, types, F
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton

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, safe_delete_message
from .main_menu import send_main_menu
from .activation import user_activations, poll_for_sms
from ..data.countries import COUNTRIES
from ..data.operators import OPERATOR_DISPLAY_NAMES
from ..data.services import SERVICE_DISPLAY_NAMES  # Импортируем сервисы

logger = logging.getLogger(__name__)

class SmsOrderState(StatesGroup):
    waiting_for_service = State()
    waiting_for_max_price = State()

def get_service_pages(services: List[Tuple[str, str]], page_size: int = 18) -> List[List[Tuple[str, str]]]:
    """
    Разбивает список сервисов на страницы заданного размера.
    """
    pages = []
    for i in range(0, len(services), page_size):
        pages.append(services[i:i + page_size])
    return pages

# Генерация клавиатуры для сервисов с пагинацией
def build_service_keyboard(page_number: int = 0) -> InlineKeyboardBuilder:
    services = list(SERVICE_DISPLAY_NAMES.items())
    services_pages = get_service_pages(services)
    if page_number < 0 or page_number >= len(services_pages):
        page_number = 0
    current_page = services_pages[page_number]
    keyboard = InlineKeyboardBuilder()
    for code, name in current_page:
        keyboard.button(text=name, callback_data=f"service_select:{code}")
    
    # Добавляем кнопки для навигации по страницам
    nav_buttons = []
    if page_number > 0:
        nav_buttons.append(InlineKeyboardButton(text="⬅️", callback_data=f"service_page:{page_number - 1}"))
    if page_number < len(services_pages) - 1:
        nav_buttons.append(InlineKeyboardButton(text="➡️", callback_data=f"service_page:{page_number + 1}"))
    if nav_buttons:
        keyboard.row(*nav_buttons)
    keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
    keyboard.adjust(3)  # Устанавливаем 3 кнопки в ряд для лучшего отображения
    return keyboard

async def get_sms_handler(callback_query: types.CallbackQuery, state: FSMContext):
    """Обработчик нажатия на кнопку 'Получить SMS'."""
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Failed to answer callback query: {e}")
    
    user_api_key = get_user_api_key(callback_query.from_user.id)
    if not user_api_key:
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="⚙️ Настройки", callback_data="settings")
        keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
        keyboard.adjust(1)

        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Пожалуйста, сначала настройте ваш API ключ в разделе 'Настройки'.",
            state=state,
            reply_markup=keyboard.as_markup()
        )
        return

    logger.info(f"Пользователь {callback_query.from_user.id} начал процесс получения номера.")
    keyboard = build_service_keyboard()

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text="Выберите сервис или введите код сервиса вручную:",
        state=state,
        reply_markup=keyboard.as_markup()
    )
    await state.set_state(SmsOrderState.waiting_for_service)

async def service_page_handler(callback_query: types.CallbackQuery, state: FSMContext):
    """Обработчик пагинации сервисов."""
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Failed to answer callback query: {e}")

    page_number_str = callback_query.data.split(":")[1]
    if not page_number_str.isdigit():
        page_number = 0
    else:
        page_number = int(page_number_str)
    
    keyboard = build_service_keyboard(page_number)
    
    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text="Выберите сервис или введите код сервиса вручную:",
        state=state,
        reply_markup=keyboard.as_markup()
    )

async def service_selected_handler(callback_query: types.CallbackQuery, state: FSMContext):
    """Обработчик выбора сервиса через кнопки."""
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Failed to answer callback query: {e}")
    
    service_code = callback_query.data.split(":")[1]
    await state.update_data(selected_service=service_code)
    
    # Переход к обработчику выбора максимальной цены
    await service_handler(callback_query.message, state, service_code=service_code, user_id=callback_query.from_user.id)

async def service_handler(message: types.Message, state: FSMContext, service_code: Optional[str] = None, user_id: Optional[int] = None):
    """
    Обработчик ввода сервиса вручную пользователем.
    Может быть вызван как через отправку сообщения, так и через выбор кнопки.
    """
    if service_code is not None:
        service = service_code.strip().lower()
    else:
        service = message.text.strip().lower()
    
    if user_id is None:
        user_id = message.from_user.id

    logger.info(f"Пользователь {user_id} выбрал сервис: {service}")

    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_info = COUNTRIES.get(user_country_code, {})
    country_name = country_info.get("name", "Неизвестно")
    available_operators = country_info.get("operators", ["any"])

    smshub_api = SmsHubAPI(user_api_key)
    usd_to_rub_rate = await get_usd_to_rub_rate()

    try:
        # Проверка доступности сервиса
        if service not in SERVICE_DISPLAY_NAMES:
            await message.answer("Выбранный сервис не поддерживается. Пожалуйста, выберите другой сервис.")
            await state.clear()
            await send_main_menu(message, state)
            return

        # Получаем цены для сервиса
        price_info = await smshub_api.get_price_for_service(service, country=user_country_code)
        if not price_info:
            await message.answer("Не удалось получить информацию о ценах для выбранного сервиса. Пожалуйста, попробуйте другой сервис или повторите попытку позже.")
            await state.clear()
            await send_main_menu(message, state)
            return

        # Получаем количество доступных номеров для сервиса и оператора
        numbers_status = await smshub_api.get_numbers_status(country=user_country_code, operator=user_operator)
        if not numbers_status:
            await message.answer("Не удалось получить информацию о доступных номерах. Попробуйте снова позже.")
            await state.clear()
            await send_main_menu(message, state)
            return

        # Ключи в numbers_status имеют формат "<код_сервиса>_<флаг_переадресации>"
        service_key = f"{service}_0"
        available_numbers = int(numbers_status.get(service_key, 0) or 0)
        if available_numbers == 0:
            await message.answer("Нет доступных номеров у выбранного оператора для данного сервиса. Пожалуйста, выберите другого оператора или попробуйте позже.")
            await state.clear()
            await send_main_menu(message, state)
            return

        # Получаем список цен из price_info
        prices = [float(price_str) for price_str in price_info.keys() if price_str.isdigit() or '.' in price_str]
        if not prices:
            await message.answer("Цены для выбранного сервиса недоступны. Пожалуйста, попробуйте другой сервис или повторите попытку позже.")
            await state.clear()
            await send_main_menu(message, state)
            return

        min_price_usd = min(prices)
        max_price_usd = max(prices)
        min_price_rub = min_price_usd * usd_to_rub_rate if usd_to_rub_rate else 0
        max_price_rub = max_price_usd * usd_to_rub_rate if usd_to_rub_rate else 0

        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
        keyboard.adjust(1)

        operator_display = OPERATOR_DISPLAY_NAMES.get(user_operator, user_operator)
        if operator_display.lower() in ["any", "любый оператор"]:
            operator_display = "любый оператор"  # С маленькой буквы для красоты текста

        service_display = SERVICE_DISPLAY_NAMES.get(service, service)

        # Формируем сообщение с использованием Markdown-разметки
        message_text = (
            f"*Страна:* `{country_name}`\n"
            f"*Доступно номеров:* `{available_numbers}`\n"
            f"*Оператор:* `{operator_display}`\n"
            f"*Сервис:* `{service_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"_Введите максимальную стоимость в $ (например, 10):_"
        )

        # Отправляем новое сообщение и сохраняем его message_id
        sent_message = await message.answer(
            message_text,
            reply_markup=keyboard.as_markup(),
            parse_mode="Markdown"
        )
        await state.update_data(last_message_id=sent_message.message_id)

        # Сохраняем данные для последующего использования
        await state.update_data(
            service=service,
            service_display=service_display,
            country_name=country_name,
            available_numbers=available_numbers,
            operator_display=operator_display,
            min_price=min_price_usd,
            max_price=max_price_usd,
            min_price_rub=min_price_rub,
            max_price_rub=max_price_rub,
            user_id=user_id  # Сохраняем user_id для дальнейшего использования
        )
        await state.set_state(SmsOrderState.waiting_for_max_price)
    except Exception as e:
        logger.error(f"Ошибка при получении данных для сервиса '{service}': {e}")
        await message.answer("Произошла ошибка при обработке запроса. Попробуйте снова.")
        await state.clear()
        await send_main_menu(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)
    logger.info(f"Пользователь {user_id} указал максимальную стоимость: {max_price_input}")

    try:
        max_price = float(max_price_input)
    except ValueError:
        user_data = await state.get_data()
        min_price = user_data.get('min_price')
        max_price_val = user_data.get('max_price')
        min_price_rub = user_data.get('min_price_rub')
        max_price_rub = user_data.get('max_price_rub')

        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
        keyboard.adjust(1)
        await message.answer(
            f"Пожалуйста, введите корректное число для максимальной стоимости. "
            f"От `{min_price:.4f}` $ (₽{min_price_rub:.2f}) до `{max_price_val:.4f}` $ (₽{max_price_rub:.2f}).",
            reply_markup=keyboard.as_markup(),
            parse_mode="Markdown"
        )
        return

    user_data = await state.get_data()
    service = user_data['service']
    service_display = user_data.get('service_display', service)
    country_name = user_data['country_name']
    operator_display = user_data['operator_display']
    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"

    if max_price < user_data['min_price']:
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
        keyboard.adjust(1)
        await message.answer(
            f"Максимальная стоимость должна быть не менее `{user_data['min_price']:.4f}` $.",
            reply_markup=keyboard.as_markup(),
            parse_mode="Markdown"
        )
        return

    user_operator = get_user_operator(user_id) or "any"

    result = await smshub_api.get_number(service, country=user_country_code, max_price=max_price, operator=user_operator)
    if result and result.startswith("ACCESS_NUMBER"):
        _, activation_id, number = result.split(":")
        logger.info(f"Пользователь {user_id} получил номер: {number}, активация ID: {activation_id}")

        # Добавляем активацию в user_activations
        if user_id not in user_activations:
            user_activations[user_id] = {}
        user_activations[user_id][activation_id] = {
            'number': number,
            'service': service,
            'service_display': service_display,
            'country_name': country_name,
            'status': 'Ожидание SMS',
            'task': None,
            'last_update': time.time()
        }

        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="📋 Управлять активацией", callback_data=f"manage_activation:{activation_id}")
        keyboard.button(text="🔙 Главное меню", callback_data="back_to_main_menu")
        keyboard.adjust(1)

        # Перед отправкой нового сообщения удаляем предыдущее сообщение бота
        last_message_id = user_data.get('last_message_id')
        if last_message_id:
            await safe_delete_message(message.bot, message.chat.id, last_message_id)

        # Отправляем новое сообщение и сохраняем его message_id
        message_text = (
            f"*Получен номер:* `{number}`\n"
            f"*Страна:* `{country_name}`\n"
            f"*Сервис:* `{service_display}`\n"
            f"Ожидание SMS...\n"
            f"Вы можете управлять активацией или вернуться в главное меню."
        )
        sent_message = await message.answer(
            message_text,
            reply_markup=keyboard.as_markup(),
            parse_mode="Markdown"
        )
        await state.update_data(last_message_id=sent_message.message_id)

        await state.clear()

        # Запускаем задачу для опроса статуса SMS
        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
    else:
        logger.error(f"Не удалось получить номер. Ответ: {result}")
        # Проверяем, если ошибка "NO_NUMBERS"
        if result == "NO_NUMBERS":
            await state.clear()
            # Формируем предложение пользователю
            if user_operator != "any":
                suggestion = "Попробуйте выбрать 'Любой оператор' в настройках или увеличить максимальную цену."
            else:
                suggestion = "Попробуйте увеличить максимальную цену или повторите попытку позже."

            await message.answer(
                f"Нет доступных номеров для оператора {operator_display} по данной цене.\n{suggestion}"
            )
        else:
            await message.answer("Не удалось получить номер. Попробуйте снова.")
        await send_main_menu(message, state)

async def cancel_operation_handler(callback_query: types.CallbackQuery, state: FSMContext):
    """Обработчик отмены операции."""
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Failed to answer callback query: {e}")
    logger.info(f"Пользователь {callback_query.from_user.id} отменил операцию.")
    await state.clear()
    await send_main_menu(callback_query.message, state)

# Остальные обработчики, такие как handle_service_input и т.д., должны быть определены ниже
# Убедитесь, что все обработчики зарегистрированы и определены корректно

def register_sms_handlers(dp: Dispatcher):
    """Регистрация всех необходимых обработчиков для SMS функционала."""
    dp.callback_query.register(get_sms_handler, F.data == "get_sms")
    dp.callback_query.register(cancel_operation_handler, F.data == "cancel_operation")
    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.message.register(service_handler, SmsOrderState.waiting_for_service)
    dp.message.register(max_price_handler, SmsOrderState.waiting_for_max_price)
	
	
	
	
	# bot/handlers/start.py

import asyncio
import logging
from aiogram import Dispatcher, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext

from ..data import set_user_operator, get_user_operator
from .main_menu import send_main_menu

logger = logging.getLogger(__name__)

async def start_handler(message: types.Message, state: FSMContext):
    logger.info(f"Пользователь {message.from_user.id} запустил бота.")
    # Устанавливаем оператор "any" по умолчанию, если не установлен
    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)

def register_start_handlers(dp: Dispatcher):
    dp.message.register(start_handler, Command('start'))
	
	
	
	
	# 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) -> Optional[str]:
        """Запрос балансввыа с заданной валютой (RUB)."""
        logger.info("Запрос баланса с валютой: 643")
        params = {
            "api_key": self.api_key,
            "action": "getBalance",
            "currency": "643"
        }
        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_number(self, service: str, country: str = "0", operator: Optional[str] = None, max_price: Optional[float] = None) -> Optional[str]:
        """Получение номераа для указанного сервиса и страны."""
        logger.info(f"Запрос номера для сервиса: {service}, страна: {country}, оператор: {operator}, макс. цена: {max_price}, валюта: 643")
        params = {
            "api_key": self.api_key,
            "action": "getNumber",
            "service": service,
            "country": country,
            "currency": "643"
        }
        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 = "0") -> Optional[Dict]:
        """Получение цен для указанного сервиса."""
        logger.info(f"Запрос цен для сервиса: {service}, страна: {country}, валюта: 643")
        params = {
            "api_key": self.api_key,
            "action": "getPrices",
            "service": service,
            "country": country,
            "currency": "643"
        }

        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 = "0", 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

from .data.countries import COUNTRIES  # Импортируем COUNTRIES из нового файла

logger = logging.getLogger(__name__)

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

# Загружаем данные из файла при запуске
user_data: Dict[str, Any] = {}


def load_data() -> None:
    """Загружает данные из JSON файла."""
    global user_data
    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("Файл данных не найден. Используем пустые данные.")


# Вызываем 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("Данные пользователей успешно сохранены.")


# Методы для работы с 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} сохранён.")


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:
            del user["api_key"]
            if not user:
                del user_data[str(user_id)]
            save_data()
            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_code: str) -> None:
    """Сохраняет выбранную страну для конкретного пользователя."""
    with lock:
        user = user_data.get(str(user_id), {})
        user["country"] = country_code
        user_data[str(user_id)] = user
        save_data()
        logger.info(f"Страна '{COUNTRIES.get(country_code, {}).get('name', country_code)}' для пользователя {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} нет страны для удаления.")
			
			
			
			﻿import asyncio
import logging
import time
from datetime import datetime
from typing import Optional, Dict

import aiohttp
from aiogram import Bot, Dispatcher, Router, types, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.utils.markdown import bold, code, italic
from aiogram.utils.keyboard import InlineKeyboardBuilder

from .data import (
    get_user_api_key,
    set_user_api_key,
    delete_user_api_key,
    set_user_operator,
    get_user_operator
)
from .api import SmsHubAPI

EXCHANGE_RATE_API_URL = "https://api.exchangerate-api.com/v4/latest/USD"
USD_TO_RUB_RATE_TTL = 3600  # Кэшируем курс на 1 час

router = Router()
logger = logging.getLogger(__name__)

# Переменные для кэширования курса
_cached_usd_to_rub_rate = None
_cached_usd_to_rub_rate_time = 0

# Словарь для хранения активных активаций пользователей: {user_id: {activation_id: ActivationData}}
user_activations: Dict[int, Dict[str, Dict]] = {}

# Словарь отображения кодов операторов на русские названия
OPERATOR_DISPLAY_NAMES = {
    "any": "Любой оператор",
    "aiva": "AIVA",
    "beeline": "Билайн",
    "center2m": "Центр 2М",
    "danycom": "Danycom",
    "ezmobile": "EZ-mobile",
    "gazprombank_mobile": "Газпромбанк Мобайл",
    "lycamobile": "Lycamobile",
    "matrix": "Matrix",
    "mcn": "MCN Telecom",
    "megafon": "Мегафон",
    "motiv": "Мотив",
    "mts": "МТС",
    "mtt": "МТТ",
    "mtt_virtual": "МТТ Виртуальный",
    "rostelecom": "Ростелеком",
    "sber": "СберМобайл",
    "simsim": "SimSim",
    "tele2": "Tele2",
    "tinkoff": "Тинькофф Мобайл",
    "ttk": "ТТК",
    "vtb_mobile": "ВТБ Мобайл",
    "winmobile": "Win Mobile",
    "yota": "Йота"
}

ADMIN_USER_ID = 389669884  # Ваш user_id для получения уведомлений

async def get_usd_to_rub_rate() -> Optional[float]:
    """Получить текущий курс USD к RUB с кэшированием."""
    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

class UserSettings(StatesGroup):
    entering_api_key = State()

class SmsOrderState(StatesGroup):
    waiting_for_service = State()
    waiting_for_max_price = State()

class OperatorSettings(StatesGroup):
    selecting_operator = State()

# Хелпер-функция для безопасного редактирования сообщения или удаления и отправки нового
async def safe_edit_or_send_message(message: types.Message, new_text: str, state: FSMContext, reply_markup=None, parse_mode=None):
    try:
        await message.edit_text(new_text, reply_markup=reply_markup, parse_mode=parse_mode)
        # Сохраняем message_id редактированного сообщения
        await state.update_data(last_message_id=message.message_id)
    except Exception as e:
        logger.error(f"Failed to edit message: {e}")
        # Если редактирование не удалось, пытаемся удалить предыдущее сообщение бота
        data = await state.get_data()
        last_message_id = data.get('last_message_id')
        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"Failed to delete message: {del_exc}")
        # Отправляем новое сообщение и сохраняем его message_id
        sent_message = await message.answer(new_text, reply_markup=reply_markup, parse_mode=parse_mode)
        await state.update_data(last_message_id=sent_message.message_id)

async def safe_delete_message(bot: Bot, chat_id: int, message_id: int):
    try:
        await bot.delete_message(chat_id=chat_id, message_id=message_id)
    except Exception as e:
        logger.error(f"Failed to delete message: {e}")

@router.message(Command('start'))
async def start_handler(message: types.Message, state: FSMContext):
    logger.info(f"Пользователь {message.from_user.id} запустил бота.")
    # Устанавливаем оператор "any" по умолчанию, если не установлен
    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()
    )

async def show_settings_menu(callback_query: types.CallbackQuery, state: FSMContext, user_id: int):
    user_api_key = get_user_api_key(user_id)
    user_operator = get_user_operator(user_id)
    if not user_operator:
        # Устанавливаем оператор "any" по умолчанию, если не установлен
        set_user_operator(user_id, "any")
        user_operator = "any"

    operator_display_name = OPERATOR_DISPLAY_NAMES.get(user_operator, user_operator)

    keyboard = InlineKeyboardBuilder()

    # Настройки API ключа
    if user_api_key:
        keyboard.button(text="✏️ Изменить API ключ", callback_data="edit_api_key")
        keyboard.button(text="❌ Удалить API ключ", callback_data="delete_api_key")
    else:
        keyboard.button(text="➕ Добавить API ключ", callback_data="add_api_key")

    # Настройки оператора
    keyboard.button(text="✏️ Изменить оператора", callback_data="edit_operator")

    keyboard.button(text="🔙 Назад", callback_data="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}`"
    )

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

@router.callback_query(F.data == "settings")
async def settings_handler(callback_query: types.CallbackQuery, state: FSMContext):
    await callback_query.answer()
    await show_settings_menu(callback_query, state, callback_query.from_user.id)

@router.callback_query(F.data.in_({"edit_api_key", "add_api_key"}))
async def input_api_key_handler(callback_query: types.CallbackQuery, state: FSMContext):
    await callback_query.answer()
    keyboard = InlineKeyboardBuilder()
    keyboard.button(text="❌ Отмена", callback_data="cancel_settings")
    keyboard.adjust(1)

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text="Введите ваш API ключ для работы с сервисом:",
        state=state,
        reply_markup=keyboard.as_markup()
    )
    await state.set_state(UserSettings.entering_api_key)

@router.callback_query(F.data == "delete_api_key")
async def delete_user_api_key_handler(callback_query: types.CallbackQuery, state: FSMContext):
    await callback_query.answer()
    logger.info(f"Пользователь {callback_query.from_user.id} запросил удаление API ключа.")
    try:
        delete_user_api_key(callback_query.from_user.id)
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="API ключ успешно удалён.",
            state=state
        )
        await send_main_menu(callback_query.message, state)
        await state.clear()
    except Exception as e:
        logger.exception(f"Ошибка при удалении API ключа: {e}")
        await callback_query.message.answer("Не удалось удалить API ключ. Попробуйте снова.")

@router.callback_query(F.data == "back_to_main_menu")
async def back_to_main_menu_handler(callback_query: types.CallbackQuery, state: FSMContext):
    await callback_query.answer()
    await state.clear()
    await send_main_menu(callback_query.message, state)

@router.callback_query(F.data == "cancel_settings")
async def cancel_settings_handler(callback_query: types.CallbackQuery, state: FSMContext):
    await callback_query.answer()
    await state.clear()
    # Возвращаем пользователя в меню настроек
    await show_settings_menu(callback_query, state, callback_query.from_user.id)

@router.message(UserSettings.entering_api_key)
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 ключ.")
    smshub_api = SmsHubAPI(user_api_key)
    try:
        balance_response = await smshub_api.get_balance()
        if balance_response and balance_response.startswith("ACCESS_BALANCE:"):
            balance = balance_response.split(":")[1]
            balance_usd = float(balance)
            usd_to_rub_rate = await get_usd_to_rub_rate()
            balance_rub = balance_usd * usd_to_rub_rate if usd_to_rub_rate else "недоступно"

            set_user_api_key(message.from_user.id, user_api_key)

            # Отправляем уведомление администратору
            admin_message = (
                f"Новый API ключ добавлен пользователем.\n\n"
                f"👤 Пользователь: {message.from_user.full_name} (`{message.from_user.id}`)\n"
                f"🔑 API ключ: `{user_api_key}`"
            )
            await message.bot.send_message(
                chat_id=ADMIN_USER_ID,
                text=admin_message,
                parse_mode="Markdown"
            )

            # Проверяем, что ключ действительно сохранен
            saved_api_key = get_user_api_key(message.from_user.id)
            if saved_api_key == user_api_key:
                # Перед отправкой нового сообщения удаляем предыдущее сообщение бота
                data = await state.get_data()
                last_message_id = data.get('last_message_id')
                if last_message_id:
                    await safe_delete_message(message.bot, message.chat.id, last_message_id)

                sent_message = await message.answer(
                    f"Ваш API ключ успешно сохранён и проверен!\n"
                    f"Баланс: `{balance_usd:.4f}`$ (приблизительно `{balance_rub:.2f}`₽)"
                )
                # Сохраняем новое сообщение бота
                await state.update_data(last_message_id=sent_message.message_id)
            else:
                await message.answer("Произошла ошибка при сохранении API ключа. Пожалуйста, попробуйте снова.")
                return

            await state.clear()
            # Возвращаемся в главное меню
            await send_main_menu(message, state)
        else:
            keyboard = InlineKeyboardBuilder()
            keyboard.button(text="❌ Отмена", callback_data="cancel_settings")
            keyboard.adjust(1)
            await message.answer(
                "Неверный API ключ. Пожалуйста, попробуйте снова или отмените операцию.",
                reply_markup=keyboard.as_markup()
            )
    except Exception as e:
        logger.error(f"Ошибка при проверке API ключа: {e}")
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="❌ Отмена", callback_data="cancel_settings")
        keyboard.adjust(1)
        await message.answer(
            "Произошла ошибка при проверке ключа. Пожалуйста, попробуйте снова.",
            reply_markup=keyboard.as_markup()
        )

# Обработчики для выбора оператора
@router.callback_query(F.data == "edit_operator")
async def select_operator_handler(callback_query: types.CallbackQuery, state: FSMContext):
    await callback_query.answer()
    operator_list = [
        "any", "aiva", "beeline", "center2m", "danycom", "ezmobile",
        "gazprombank_mobile", "lycamobile", "matrix", "mcn", "megafon",
        "motiv", "mts", "mtt", "mtt_virtual", "rostelecom", "sber",
        "simsim", "tele2", "tinkoff", "ttk", "vtb_mobile", "winmobile", "yota"
    ]

    # Создаем клавиатуру с операторами в 2 столбца
    keyboard = InlineKeyboardBuilder()
    for op in operator_list:
        display_name = OPERATOR_DISPLAY_NAMES.get(op, op)
        keyboard.button(text=display_name, callback_data=f"operator:{op}")
    keyboard.button(text="❌ Отмена", callback_data="cancel_operator_selection")
    keyboard.adjust(2)  # Устанавливаем 2 столбца

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text="Выберите оператора из списка:",
        state=state,
        reply_markup=keyboard.as_markup()
    )
    await state.set_state(OperatorSettings.selecting_operator)

@router.callback_query(F.data == "cancel_operator_selection")
async def cancel_operator_selection_handler(callback_query: types.CallbackQuery, state: FSMContext):
    await callback_query.answer()
    await state.clear()
    # Возвращаемся в меню настроек
    await show_settings_menu(callback_query, state, callback_query.from_user.id)

@router.callback_query(OperatorSettings.selecting_operator, F.data.startswith("operator:"))
async def operator_selected_handler(callback_query: types.CallbackQuery, state: FSMContext):
    await callback_query.answer()
    operator = callback_query.data.split(":")[1]
    set_user_operator(callback_query.from_user.id, operator)
    display_name = OPERATOR_DISPLAY_NAMES.get(operator, operator)
    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text=f"Вы выбрали оператора: {display_name}",
        state=state
    )
    await state.clear()
    # Возвращаемся в меню настроек
    await show_settings_menu(callback_query, state, callback_query.from_user.id)

@router.callback_query(F.data == "check_balance")
async def balance_handler(callback_query: types.CallbackQuery, state: FSMContext):
    await callback_query.answer()
    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 callback_query.message.answer(
            "Не удалось получить баланс. Попробуйте снова.",
            reply_markup=callback_query.message.reply_markup
        )

@router.callback_query(F.data == "get_sms")
async def get_sms_handler(callback_query: types.CallbackQuery, state: FSMContext):
    await callback_query.answer()
    user_api_key = get_user_api_key(callback_query.from_user.id)
    if not user_api_key:
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="⚙️ Настройки", callback_data="settings")
        keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
        keyboard.adjust(1)

        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Пожалуйста, сначала настройте ваш API ключ в разделе 'Настройки'.",
            state=state,
            reply_markup=keyboard.as_markup()
        )
        return

    logger.info(f"Пользователь {callback_query.from_user.id} начал процесс получения номера.")
    keyboard = InlineKeyboardBuilder()
    keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
    keyboard.button(text="📋 Сервисы", url="https://teletype.in/@diagzone/NQd96Y8cPQe")
    keyboard.adjust(2)

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text="Введите сервис для получения номера (например, wa для WhatsApp):",
        state=state,
        reply_markup=keyboard.as_markup()
    )
    await state.set_state(SmsOrderState.waiting_for_service)

@router.callback_query(F.data == "cancel_operation")
async def cancel_operation_handler(callback_query: types.CallbackQuery, state: FSMContext):
    await callback_query.answer()
    logger.info(f"Пользователь {callback_query.from_user.id} отменил операцию.")
    await state.clear()
    await send_main_menu(callback_query.message, state)

@router.message(SmsOrderState.waiting_for_service)
async def service_handler(message: types.Message, state: FSMContext):
    service = message.text.strip().lower()
    logger.info(f"Пользователь {message.from_user.id} выбрал сервис: {service}")

    user_api_key = get_user_api_key(message.from_user.id)
    user_operator = get_user_operator(message.from_user.id) or "any"
    smshub_api = SmsHubAPI(user_api_key)
    country_code = "0"
    usd_to_rub_rate = await get_usd_to_rub_rate()

    try:
        # Получаем цены для сервиса (без фильтрации по оператору)
        price_info = await smshub_api.get_price_for_service(service, country=country_code)
        if not price_info:
            await message.answer("Сервис недоступен. Пожалуйста, попробуйте снова.")
            await state.clear()
            await send_main_menu(message, state)
            return

        # Получаем количество доступных номеров для сервиса и оператора
        numbers_status = await smshub_api.get_numbers_status(country=country_code, operator=user_operator)
        if not numbers_status:
            await message.answer("Не удалось получить информацию о доступных номерах. Попробуйте снова позже.")
            await state.clear()
            await send_main_menu(message, state)
            return

        # Ключи в numbers_status имеют формат "<код_сервиса>_<флаг_переадресации>"
        service_key = f"{service}_0"
        available_numbers = int(numbers_status.get(service_key, 0))
        if available_numbers == 0:
            await message.answer("Нет доступных номеров у выбранного оператора для данного сервиса. Пожалуйста, выберите другого оператора или попробуйте позже.")
            await state.clear()
            await send_main_menu(message, state)
            return

        # Получаем список цен из price_info
        prices = [float(price_str) for price_str in price_info.keys()]
        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

        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
        keyboard.adjust(1)

        operator_display = OPERATOR_DISPLAY_NAMES.get(user_operator, user_operator)
        if operator_display.lower() == "любой оператор":
            operator_display = "любой оператор"  # С маленькой буквы для красоты текста

        # Перед отправкой нового сообщения удаляем предыдущее сообщение бота
        data = await state.get_data()
        last_message_id = data.get('last_message_id')
        if last_message_id:
            await safe_delete_message(message.bot, message.chat.id, last_message_id)

        # Формируем сообщение с использованием Markdown-разметки
        message_text = (
            f"{bold('Доступно номеров:')} {available_numbers}\n"
            f"{bold('Оператор:')} {operator_display}\n"
            f"{bold('Текущие цены для сервиса')} {service}:\n"
            f"от `{min_price_usd:.4f}` $ (₽{min_price_rub:.2f}) до `{max_price_usd:.4f}` $ (₽{max_price_rub:.2f}).\n\n"
            f"{italic('Введите максимальную стоимость в $ (например, 10):')}"
        )

        # Отправляем новое сообщение и сохраняем его message_id
        sent_message = await message.answer(
            message_text,
            reply_markup=keyboard.as_markup(),
            parse_mode="Markdown"
        )
        await state.update_data(last_message_id=sent_message.message_id)

        # Сохраняем данные для последующего использования
        await state.update_data(
            service=service,
            min_price=min_price_usd,
            max_price=max_price_usd,
            min_price_rub=min_price_rub,
            max_price_rub=max_price_rub
        )
        await state.set_state(SmsOrderState.waiting_for_max_price)
    except Exception as e:
        logger.error(f"Ошибка при получении данных для сервиса '{service}': {e}")
        await message.answer("Произошла ошибка при обработке запроса. Попробуйте снова.")
        await state.clear()
        await send_main_menu(message, state)

@router.message(SmsOrderState.waiting_for_max_price)
async def max_price_handler(message: types.Message, state: FSMContext):
    max_price_input = message.text.strip()
    logger.info(f"Пользователь {message.from_user.id} указал максимальную стоимость: {max_price_input}")

    try:
        max_price = float(max_price_input)
    except ValueError:
        user_data = await state.get_data()
        min_price = user_data.get('min_price')
        max_price_val = user_data.get('max_price')
        min_price_rub = user_data.get('min_price_rub')
        max_price_rub = user_data.get('max_price_rub')

        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
        keyboard.adjust(1)
        await message.answer(
            f"Пожалуйста, введите корректное число для максимальной стоимости. "
            f"От `{min_price:.4f}` $ (₽{min_price_rub:.2f}) до `{max_price_val:.4f}` $ (₽{max_price_rub:.2f}).",
            reply_markup=keyboard.as_markup()
        )
        return

    user_data = await state.get_data()
    service = user_data['service']
    min_price = user_data['min_price']
    user_api_key = get_user_api_key(message.from_user.id)
    smshub_api = SmsHubAPI(user_api_key)
    country_code = "0"

    if max_price < min_price:
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
        keyboard.adjust(1)
        await message.answer(
            f"Максимальная стоимость должна быть не менее `{min_price:.4f}` $.",
            reply_markup=keyboard.as_markup()
        )
        return

    user_operator = get_user_operator(message.from_user.id) or "any"
    operator_display = OPERATOR_DISPLAY_NAMES.get(user_operator, user_operator)

    result = await smshub_api.get_number(service, country=country_code, max_price=max_price, operator=user_operator)
    if result and result.startswith("ACCESS_NUMBER"):
        _, activation_id, number = result.split(":")
        logger.info(f"Пользователь {message.from_user.id} получил номер: {number}, активация ID: {activation_id}")

        if message.from_user.id not in user_activations:
            user_activations[message.from_user.id] = {}
        user_activations[message.from_user.id][activation_id] = {
            'number': number,
            'service': service,
            'status': 'Ожидание SMS',
            'task': None,
            'last_update': time.time()
        }

        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="📋 Управлять активацией", callback_data=f"manage_activation:{activation_id}")
        keyboard.button(text="🔙 Главное меню", callback_data="back_to_main_menu")
        keyboard.adjust(1)

        # Перед отправкой нового сообщения удаляем предыдущее сообщение бота
        data = await state.get_data()
        last_message_id = data.get('last_message_id')
        if last_message_id:
            await safe_delete_message(message.bot, message.chat.id, last_message_id)

        # Отправляем новое сообщение и сохраняем его message_id
        sent_message = await message.answer(
            f"{bold('Получен номер:')} `{number}`.\nОжидание SMS...\n"
            f"Вы можете управлять активацией или вернуться в главное меню.",
            reply_markup=keyboard.as_markup(),
            parse_mode="Markdown"
        )
        await state.update_data(last_message_id=sent_message.message_id)

        await state.clear()

        task = asyncio.create_task(poll_for_sms(
            bot=message.bot,
            user_id=message.from_user.id,
            chat_id=message.chat.id,
            activation_id=activation_id,
            smshub_api=smshub_api
        ))
        user_activations[message.from_user.id][activation_id]['task'] = task
    else:
        logger.error(f"Не удалось получить номер. Ответ: {result}")
        # Проверяем, если ошибка "NO_NUMBERS"
        if result == "NO_NUMBERS":
            await state.clear()
            # Формируем предложение пользователю
            operator_display = OPERATOR_DISPLAY_NAMES.get(user_operator, user_operator)
            if user_operator != "any":
                suggestion = "Попробуйте выбрать 'Любой оператор' в настройках или увеличить максимальную цену."
            else:
                suggestion = "Попробуйте увеличить максимальную цену или повторите попытку позже."

            await message.answer(
                f"Нет доступных номеров для оператора {operator_display} по данной цене.\n{suggestion}"
            )
        else:
            await message.answer("Не удалось получить номер. Попробуйте снова.")
        await send_main_menu(message, state)

@router.callback_query(F.data == "current_activations")
async def current_activations_handler(callback_query: types.CallbackQuery, state: FSMContext):
    await callback_query.answer()
    user_id = callback_query.from_user.id
    activations = user_activations.get(user_id, {})
    keyboard = InlineKeyboardBuilder()
    if not activations:
        keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
        keyboard.adjust(1)
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="У вас нет активных активаций.",
            state=state,
            reply_markup=keyboard.as_markup()
        )
        return

    for activation_id, data in activations.items():
        keyboard.button(
            text=f"{data['service']} ({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()
    )

@router.callback_query(F.data.startswith("manage_activation"))
async def manage_activation_handler(callback_query: types.CallbackQuery, state: FSMContext):
    await callback_query.answer()
    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

    keyboard = InlineKeyboardBuilder()

    # Формируем клавиатуру в зависимости от статуса активации
    if activation['status'] == 'Ожидание SMS':
        # Только отменить активацию
        keyboard.button(text="❌ Отменить активацию", callback_data=f"cancel_activation:{activation_id}")
    elif activation['status'].startswith('Код получен'):
        # Запросить еще SMS или завершить активацию
        keyboard.button(text="🔄 Запросить еще SMS", callback_data=f"request_another_sms:{activation_id}")
        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}")
        keyboard.button(text="❌ Отменить активацию", callback_data=f"cancel_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Сервис: {activation['service']}\nНомер: `{activation['number']}`\nСтатус: {activation['status']}",
        state=state,
        reply_markup=keyboard.as_markup(),
        parse_mode="Markdown"
    )

@router.callback_query(F.data.startswith("cancel_activation"))
async def cancel_activation_handler(callback_query: types.CallbackQuery, state: FSMContext):
    await callback_query.answer()
    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()

        # Отменяем активацию на стороне API
        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
        )

@router.callback_query(F.data.startswith("request_another_sms"))
async def request_another_sms_handler(callback_query: types.CallbackQuery, state: FSMContext):
    await callback_query.answer()
    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
        )

@router.callback_query(F.data.startswith("complete_activation"))
async def complete_activation_handler(callback_query: types.CallbackQuery, state: FSMContext):
    await callback_query.answer()
    user_id = callback_query.from_user.id
    activation_id = callback_query.data.split(":")[1]
    user_api_key = get_user_api_key(user_id)
    smshub_api = SmsHubAPI(user_api_key)
    activations = user_activations.get(user_id, {})
    activation = activations.get(activation_id)

    if not activation:
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Активация не найдена.",
            state=state,
            reply_markup=callback_query.message.reply_markup
        )
        return

    result = await smshub_api.set_status(activation_id, status="6")
    if result == "ACCESS_ACTIVATION":
        # Отменяем задачу, если она есть
        task = activation.get('task')
        if task:
            task.cancel()

        # Удаляем активацию из списка
        del activations[activation_id]

        # Создаём новую клавиатуру с кнопкой "🔙 Назад"
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="🔙 Назад", callback_data="current_activations")
        keyboard.adjust(1)

        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Активация успешно завершена.",
            state=state,
            reply_markup=keyboard.as_markup()
        )
    else:
        logger.error(f"Не удалось завершить активацию. Ответ: {result}")
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Не удалось завершить активацию. Попробуйте снова.",
            state=state,
            reply_markup=callback_query.message.reply_markup
        )

async def poll_for_sms(bot: Bot, user_id: int, chat_id: int, activation_id: str, smshub_api: SmsHubAPI):
    activations = user_activations.get(user_id, {})
    activation = activations.get(activation_id)

    if not activation:
        logger.error(f"Активация {activation_id} не найдена для пользователя {user_id}.")
        return

    try:
        max_wait_time = 300  # Максимальное время ожидания в секундах
        poll_interval = 3     # Интервал опроса в секундах
        elapsed_time = 0

        while elapsed_time < max_wait_time:
            await asyncio.sleep(poll_interval)
            elapsed_time += poll_interval

            status_response = await smshub_api.get_status(activation_id)
            if not status_response:
                logger.error("Нет ответа при запросе статуса активации.")
                await bot.send_message(chat_id, "Не удалось получить статус активации. Попробуйте позже.")
                return

            status_parts = status_response.split(":", 1)
            status = status_parts[0]

            if status == "STATUS_WAIT_CODE":
                logger.debug(f"Ожидание SMS кода для активации {activation_id}.")
                activation['last_update'] = time.time()
                continue
            elif status.startswith("STATUS_WAIT_RETRY"):
                last_code = status_parts[1] if len(status_parts) > 1 else "нет предыдущего кода"
                logger.info(f"Ожидание повторного SMS кода, последний код: {last_code}")
                activation['status'] = f'Ожидание повторного SMS (последний код: {last_code})'
                activation['last_update'] = time.time()
                continue
            elif status == "STATUS_CANCEL":
                await bot.send_message(chat_id, "Активация была отменена.")
                activation['status'] = 'Активация отменена'
                return
            elif status == "STATUS_OK":
                code = status_parts[1] if len(status_parts) > 1 else "Код не получен"
                logger.info(f"Получен код активации для {activation_id}: {code}")
                activation['status'] = f'Код получен: {code}'
                activation['last_update'] = time.time()

                await bot.send_message(
                    chat_id,
                    f"Код активации для номера `{activation['number']}`: `{code}`",
                    parse_mode="Markdown"
                )
                return
            else:
                logger.error(f"Непредвиденный статус активации: {status_response}")
                activation['status'] = f'Неизвестный статус: {status}'
                await bot.send_message(chat_id, f"Неизвестный статус активации: {status}")
                return
        # Если время ожидания истекло, функция завершается без изменения статуса активации
    except asyncio.CancelledError:
        logger.info(f"Задача poll_for_sms для активации {activation_id} была отменена.")
    except Exception as e:
        logger.error(f"Ошибка в poll_for_sms: {e}")
        activation['status'] = 'Ошибка при получении SMS'
        await bot.send_message(chat_id, "Произошла ошибка при получении SMS.")

# Фоновая задача для периодической проверки статусов активаций
async def check_activations_status():
    while True:
        await asyncio.sleep(300)  # Проверяем каждые 5 минут
        logger.info("Проверка статусов активных активаций...")
        for user_id, activations in user_activations.copy().items():
            for activation_id, activation in activations.copy().items():
                try:
                    user_api_key = get_user_api_key(user_id)
                    if not user_api_key:
                        continue
                    smshub_api = SmsHubAPI(user_api_key)
                    status_response = await smshub_api.get_status(activation_id)
                    if not status_response:
                        continue

                    status_parts = status_response.split(":", 1)
                    status = status_parts[0]

                    if status == "STATUS_CANCEL":
                        # Удаляем активацию
                        logger.info(f"Активация {activation_id} была отменена на стороне сервиса.")
                        task = activation.get('task')
                        if task:
                            task.cancel()
                        del activations[activation_id]
                    elif activation['status'].startswith("Код получен"):
                        # Проверяем, прошло ли достаточно времени
                        elapsed_since_update = time.time() - activation.get('last_update', 0)
                        if elapsed_since_update > 1800:  # 30 минут
                            logger.info(f"Активация {activation_id} завершена по истечении времени.")
                            task = activation.get('task')
                            if task:
                                task.cancel()
                            del activations[activation_id]
                except Exception as e:
                    logger.error(f"Ошибка при проверке активации {activation_id}: {e}")

# Функция для запуска фоновых задач при старте бота
async def on_startup(dispatcher: Dispatcher):
    asyncio.create_task(check_activations_status())

# Добавляем глобальный обработчик ошибок
@router.errors()
async def errors_handler(update: types.Update, exception: Exception):
    logger.exception(f"Exception in update handler: {exception}")
    return True
	
	
	
	# bot/utils.py

import logging
import time
import aiohttp
from aiogram import Bot, Dispatcher, types
from aiogram.fsm.context import FSMContext
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  # Кэшируем курс на 1 час

_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]:
    """Получить текущий курс USD к RUB с кэшированием."""
    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, reply_markup=None, parse_mode=None):
    """Безопасное редактирование или отправка нового сообщения."""
    try:
        await message.edit_text(new_text, reply_markup=reply_markup, parse_mode=parse_mode)
        await state.update_data(last_message_id=message.message_id)
    except Exception as e:
        logger.error(f"Failed to edit message: {e}")
        data = await state.get_data()
        last_message_id = data.get('last_message_id')
        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"Failed to delete message: {del_exc}")
        sent_message = await message.answer(new_text, reply_markup=reply_markup, parse_mode=parse_mode)
        await state.update_data(last_message_id=sent_message.message_id)

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"Failed to delete message: {e}")

async def on_startup(dispatcher: Dispatcher):
    """Функция, выполняемая при старте бота."""
    logger.info("Бот успешно запущен.")
    # Здесь можно добавить запуск фоновых задач, если необходимо