все равно ошибка в консоли при выборе кнопки:


2024-10-08 13:10:24,828 - bot.handlers - INFO - User 389669884 started the process of obtaining a number.
2024-10-08 13:10:24,925 - aiogram.event - INFO - Update id=480213967 is handled. Duration 169 ms by bot id=7625597085
2024-10-08 13:10:27,242 - bot.handlers - INFO - User 389669884 selected service via button: wb
2024-10-08 13:10:27,823 - root - INFO - Запрос цен для сервиса: wb, страна: 0, валюта: 643
2024-10-08 13:10:27,824 - bot.handlers - ERROR - Error retrieving data for service 'wb': Invalid variable type: value should be str, int or float, got None of type <class 'NoneType'>
2024-10-08 13:10:27,902 - bot.handlers - INFO - User 389669884 returned to the main menu.
2024-10-08 13:10:27,976 - aiogram.event - INFO - Update id=480213968 is handled. Duration 797 ms by bot id=7625597085


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

import aiohttp
import logging
import json

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

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

    async def get_balance(self):
        logging.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:
                    logging.info(f"Ответ баланса: {text}")
                    return text
                else:
                    logging.error(f"Ошибка при запросе баланса: {response.status}, ответ: {text}")
                    return None

    async def get_number(self, service, country="0", operator=None, max_price=None):
        logging.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:
                    logging.info(f"Ответ номера: {text}")
                    return text
                else:
                    logging.error(f"Ошибка при запросе номера: {response.status}, ответ: {text}")
                    return None

    async def get_price_for_service(self, service, country="0"):
        logging.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:
                    logging.info(f"Ответ цен: {response_text}")
                    try:
                        prices_data = json.loads(response_text)
                        logging.info(f"Цены для сервиса {service}: {prices_data}")
                        # Проверяем наличие данных для указанной страны
                        country_data = prices_data.get(country)
                        if not country_data:
                            logging.error(f"Страна '{country}' отсутствует в данных: {prices_data}")
                            return None
                        # Проверяем наличие данных для указанного сервиса
                        service_data = country_data.get(service)
                        if not service_data:
                            logging.error(f"Сервис '{service}' отсутствует среди доступных для страны '{country}': {country_data}")
                            return None
                        return service_data  # Возвращаем словарь { "Цена": Количество }
                    except json.JSONDecodeError:
                        # Обрабатываем случаи, когда ответ не является JSON (например, ошибки от API)
                        logging.error(f"Ошибка при разборе JSON: {response_text}")
                        return None
                else:
                    logging.error(f"Ошибка при запросе цен: {response.status}, ответ: {response_text}")
                    return None

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

    async def get_status(self, activation_id):
        logging.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:
                    logging.info(f"Ответ статуса: {text}")
                    return text
                else:
                    logging.error(f"Ошибка при запросе статуса: {response.status}, ответ: {text}")
                    return None

    async def set_status(self, activation_id, status):
        logging.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:
                    logging.info(f"Ответ изменения статуса: {text}")
                    return text
                else:
                    logging.error(f"Ошибка при изменении статуса: {response.status}, ответ: {text}")
                    return None

    async def cancel_activation(self, activation_id):
        return await self.set_status(activation_id, status="8")


import json
import os
import threading
import logging

logger = logging.getLogger(__name__)

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

# Загружаем данные из файла при запуске
user_data = {}


def load_data():
    """Загружает данные из 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():
    """Сохраняет текущие данные в 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, api_key):
    """Сохраняет 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):
    """Возвращает API ключ для конкретного пользователя, если он сохранен."""
    with lock:
        return user_data.get(str(user_id), {}).get("api_key")


def delete_user_api_key(user_id):
    """Удаляет 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, operator):
    """Сохраняет выбранный оператор для конкретного пользователя."""
    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):
    """Возвращает выбранный оператор для конкретного пользователя, если он сохранен."""
    with lock:
        return user_data.get(str(user_id), {}).get("operator")


def delete_user_operator(user_id):
    """Удаляет выбранный оператор для конкретного пользователя."""
    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} нет оператора для удаления.")


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, 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

from .helpers import build_services_keyboard
from .services import SERVICES

EXCHANGE_RATE_API_URL = "https://api.exchangerate-api.com/v4/latest/USD"
USD_TO_RUB_RATE_TTL = 3600  # Cache exchange rate for 1 hour

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

# Variables for caching the exchange rate
_cached_usd_to_rub_rate = None
_cached_usd_to_rub_rate_time = 0

# Dictionary to store active user activations: {user_id: {activation_id: ActivationData}}
user_activations: Dict[int, Dict[str, Dict]] = {}

# Operator code to display name mapping
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  # Your user_id to receive notifications

async def get_usd_to_rub_rate() -> Optional[float]:
    """Get the current USD to RUB exchange rate with caching."""
    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 rate not found in response.")
                    return None
    except Exception as e:
        logger.error(f"Error fetching exchange rate: {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()

# Helper function for safe message editing or sending
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)
        # Save the message_id of the edited message
        await state.update_data(last_message_id=message.message_id)
    except Exception as e:
        logger.error(f"Failed to edit message: {e}")
        # If editing failed, delete the previous bot message
        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}")
        # Send a new message and save its 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"User {message.from_user.id} started the bot.")
    # Set operator to "any" by default if not set
    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"User {message.chat.id} returned to the main menu.")
    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:
        # Set operator to "any" by default if not set
        set_user_operator(user_id, "any")
        user_operator = "any"

    operator_display_name = OPERATOR_DISPLAY_NAMES.get(user_operator, user_operator)

    keyboard = InlineKeyboardBuilder()

    # API key settings
    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")

    # Operator settings
    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"User {callback_query.from_user.id} requested API key deletion.")
    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"Error deleting API key: {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()
    # Return user to settings menu
    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"User {message.from_user.id} entered API key.")
    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)

            # Send notification to admin
            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"
            )

            # Verify that the key is saved
            saved_api_key = get_user_api_key(message.from_user.id)
            if saved_api_key == user_api_key:
                # Before sending a new message, delete the previous bot message
                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}`₽)"
                )
                # Save the new bot message
                await state.update_data(last_message_id=sent_message.message_id)
            else:
                await message.answer("Произошла ошибка при сохранении API ключа. Пожалуйста, попробуйте снова.")
                return

            await state.clear()
            # Return to main menu
            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"Error validating API key: {e}")
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="❌ Отмена", callback_data="cancel_settings")
        keyboard.adjust(1)
        await message.answer(
            "Произошла ошибка при проверке ключа. Пожалуйста, попробуйте снова.",
            reply_markup=keyboard.as_markup()
        )

# Handlers for operator selection
@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"
    ]

    # Create keyboard with operators in 2 columns
    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)  # Set 2 columns

    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()
    # Return to settings menu
    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()
    # Return to settings menu
    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"User {callback_query.from_user.id} requested balance.")

    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"Failed to get balance. Response: {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"User {callback_query.from_user.id} started the process of obtaining a number.")

    # Send the services keyboard starting from page 0
    keyboard = build_services_keyboard(page_number=0)
    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text="Выберите сервис для получения номера или введите код сервиса вручную:",
        state=state,
        reply_markup=keyboard,
        parse_mode="Markdown"
    )
    await state.update_data(current_services_page=0)
    await state.set_state(SmsOrderState.waiting_for_service)

# Handle service selection from inline keyboard
@router.callback_query(SmsOrderState.waiting_for_service, F.data.startswith("service_select:"))
async def service_selected_callback_handler(callback_query: types.CallbackQuery, state: FSMContext):
    service_code = callback_query.data.split(":", 1)[1]
    await callback_query.answer()
    logger.info(f"User {callback_query.from_user.id} selected service via button: {service_code}")
    await state.update_data(service=service_code)
    await process_service_selection(callback_query.message, state)

# Handle pagination of services
@router.callback_query(SmsOrderState.waiting_for_service, F.data.startswith("services_page:"))
async def services_page_callback_handler(callback_query: types.CallbackQuery, state: FSMContext):
    page_number = int(callback_query.data.split(":", 1)[1])
    logger.info(f"User {callback_query.from_user.id} navigated to page {page_number}")
    await callback_query.answer()
    keyboard = build_services_keyboard(page_number=page_number)
    await state.update_data(current_services_page=page_number)
    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text="Выберите сервис для получения номера или введите код сервиса вручную:",
        state=state,
        reply_markup=keyboard,
        parse_mode="Markdown"
    )

# Modify the message handler for manual service input
@router.message(SmsOrderState.waiting_for_service)
async def service_handler(message: types.Message, state: FSMContext):
    service = message.text.strip().lower()
    logger.info(f"User {message.from_user.id} entered service: {service}")
    await state.update_data(service=service)
    await process_service_selection(message, state)

# Function to process the service selection
async def process_service_selection(message_or_callback, state: FSMContext):
    data = await state.get_data()
    service = data.get('service')
    if not service:
        # Handle the case where service is not set
        return

    if isinstance(message_or_callback, types.Message):
        user = message_or_callback.from_user
        message = message_or_callback
    elif isinstance(message_or_callback, types.CallbackQuery):
        user = message_or_callback.from_user
        message = message_or_callback.message
    else:
        logger.error("Unknown message_or_callback type.")
        return

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

    try:
        # Check if service is valid
        service_codes = [s['code'] for s in SERVICES]
        if service not in service_codes:
            keyboard = InlineKeyboardBuilder()
            keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
            keyboard.adjust(1)
            await message.answer(
                f"Сервис '{service}' не найден. Пожалуйста, выберите сервис из списка или введите корректный код.",
                reply_markup=keyboard.as_markup()
            )
            return

        # Get prices for the service (without filtering by operator)
        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

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

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

        # Get the list of prices from 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 = "любой оператор"  # Lowercase for better text appearance

        # Before sending a new message, delete the previous bot message
        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)

        # Formulate the message using Markdown formatting
        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):')}"
        )

        # Send a new message and save its 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)

        # Save data for later use
        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"Error retrieving data for service '{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"User {message.from_user.id} specified maximum price: {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)
    operator_param = None if user_operator == "any" else user_operator

    result = await smshub_api.get_number(service, country=country_code, max_price=max_price, operator=operator_param)
    if result and result.startswith("ACCESS_NUMBER"):
        _, activation_id, number = result.split(":")
        logger.info(f"User {message.from_user.id} received number: {number}, activation 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)

        # Before sending a new message, delete the previous bot message
        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)

        # Send a new message and save its 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"Failed to get number. Response: {result}")
        # Check if error is "NO_NUMBERS"
        if result == "NO_NUMBERS":
            await state.clear()
            # Suggestion to user
            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()

    # Form keyboard depending on activation status
    if activation['status'] == 'Ожидание SMS':
        # Only cancel activation
        keyboard.button(text="❌ Отменить активацию", callback_data=f"cancel_activation:{activation_id}")
    elif activation['status'].startswith('Код получен'):
        # Request another SMS or complete activation
        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'):
        # Complete or cancel activation
        keyboard.button(text="✅ Завершить активацию", callback_data=f"complete_activation:{activation_id}")
        keyboard.button(text="❌ Отменить активацию", callback_data=f"cancel_activation:{activation_id}")
    else:
        # Other statuses
        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:
        # Cancel task if exists
        task = activation.get('task')
        if task:
            task.cancel()

        # Cancel activation on API side
        result = await smshub_api.cancel_activation(activation_id)
        logger.info(f"Activation {activation_id} canceled on API side: {result}")

        # Remove activation from the list
        del activations[activation_id]

        # Create new keyboard with "🔙 Назад" button
        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":
        # Cancel previous task if exists
        task = activation.get('task')
        if task:
            task.cancel()

        # Start a new task
        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"Failed to request another SMS. Response: {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":
        # Cancel task if exists
        task = activation.get('task')
        if task:
            task.cancel()

        # Remove activation from the list
        del activations[activation_id]

        # Create new keyboard with "🔙 Назад" button
        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"Failed to complete activation. Response: {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 {activation_id} not found for user {user_id}.")
        return

    try:
        max_wait_time = 300  # Maximum wait time in seconds
        poll_interval = 3     # Polling interval in seconds
        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("No response when requesting activation status.")
                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"Waiting for SMS code for activation {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"Waiting for retry SMS code, last code: {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"Received activation code for {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"Unexpected activation status: {status_response}")
                activation['status'] = f'Неизвестный статус: {status}'
                await bot.send_message(chat_id, f"Неизвестный статус активации: {status}")
                return
        # If wait time has expired, function ends without changing activation status
    except asyncio.CancelledError:
        logger.info(f"Task poll_for_sms for activation {activation_id} was cancelled.")
    except Exception as e:
        logger.error(f"Error in poll_for_sms: {e}")
        activation['status'] = 'Ошибка при получении SMS'
        await bot.send_message(chat_id, "Произошла ошибка при получении SMS.")

# Background task to periodically check activation statuses
async def check_activations_status():
    while True:
        await asyncio.sleep(300)  # Check every 5 minutes
        logger.info("Checking statuses of active activations...")
        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":
                        # Remove activation
                        logger.info(f"Activation {activation_id} was cancelled on the service side.")
                        task = activation.get('task')
                        if task:
                            task.cancel()
                        del activations[activation_id]
                    elif activation['status'].startswith("Код получен"):
                        # Check if enough time has passed
                        elapsed_since_update = time.time() - activation.get('last_update', 0)
                        if elapsed_since_update > 1800:  # 30 minutes
                            logger.info(f"Activation {activation_id} completed after time expired.")
                            task = activation.get('task')
                            if task:
                                task.cancel()
                            del activations[activation_id]
                except Exception as e:
                    logger.error(f"Error checking activation {activation_id}: {e}")

# Function to start background tasks when bot starts
async def on_startup(dispatcher: Dispatcher):
    asyncio.create_task(check_activations_status())

# Add global error handler
@router.errors()
async def errors_handler(update: types.Update, exception: Exception):
    logger.exception(f"Exception in update handler: {exception}")
    return True

# Add a cancel operation handler
@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"User {callback_query.from_user.id} cancelled the operation.")
    await state.clear()
    await send_main_menu(callback_query.message, state)



# helpers.py

from math import ceil
from aiogram import types
from aiogram.utils.keyboard import InlineKeyboardBuilder
from .services import SERVICES, SERVICES_PER_PAGE

def build_services_keyboard(page_number=0):
    total_services = len(SERVICES)
    total_pages = ceil(total_services / SERVICES_PER_PAGE)
    
    start_idx = page_number * SERVICES_PER_PAGE
    end_idx = start_idx + SERVICES_PER_PAGE
    services_on_page = SERVICES[start_idx:end_idx]
    
    keyboard = InlineKeyboardBuilder()
    
    for service in services_on_page:
        keyboard.button(text=service['name'], callback_data=f"service_select:{service['code']}")
    
    keyboard.adjust(3)  # 3 columns
    
    # Navigation buttons
    navigation_buttons = []
    if page_number > 0:
        navigation_buttons.append(types.InlineKeyboardButton(text='⬅️ Предыдущая', callback_data=f'services_page:{page_number - 1}'))
    if end_idx < total_services:
        navigation_buttons.append(types.InlineKeyboardButton(text='Следующая ➡️', callback_data=f'services_page:{page_number + 1}'))
    if navigation_buttons:
        keyboard.row(*navigation_buttons)
    
    # Cancel button
    keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
    
    return keyboard.as_markup()



# services.py

from math import ceil

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

SERVICES_PER_PAGE = 9  # 3 columns x 3 rows