Respan Dataset Explorer

Select one behavior. Every returned turn has one binary label: Present or Absent. Source: final dense boolean release.

5,167,182physical rows
86shards
0.00%qualified row coverage
0.00%qualified cell coverage
Random row JSON API

turns-00032.parquet:9364

b291333f03690c7562f303d9
turn 1/1gpt-4o-2024-08-06EnglishUnited States116 words
degenerate_repetitionAbsentFinal dense release
USER
Assistant: 
User: System: I will provide you with a textual GitHub issue title of an Android app, aiming to categorize the issues into one or multiple specific intention classes:
bug: indicates an unexpected problem or unintended behavior
enhancement: Indicates new feature requests
other: Other categories
User: 'prompt:I will provide you with a textual GitHub issue title of an Android app, aiming to categorize the issues into one or multiple specific intention classes:
bug: indicates an unexpected problem or unintended behavior
enhancement: Indicates new feature requests
other: Other categories
Does the author's intention in this GitHub issue relate to a bug or enhancement or other?
issue: Tool crashes when PR is opened'
intention: '
Assistant:
Assistant:
ASSISTANT
bug

turns-00032.parquet:9365

1b2750745f25236f1557f7ae
turn 1/1gpt-4o-2024-08-06EnglishAustria163 words
degenerate_repetitionAbsentFinal dense release
USER
Assistant: 
User: System: I will provide you with a textual GitHub issue title of an Android app, aiming to categorize the issues into one or multiple specific intention classes:
bug: indicates an unexpected problem or unintended behavior
enhancement: Indicates new feature requests
other: Other categories
User: 'prompt:I will provide you with a textual GitHub issue title of an Android app, aiming to categorize the issues into one or multiple specific intention classes:
bug: indicates an unexpected problem or unintended behavior
enhancement: Indicates new feature requests
other: Other categories
Does the author's intention in this GitHub issue relate to a bug or enhancement or other?
issue: Remove search term tab groups from History'
intention: '
Assistant:
Assistant:
ASSISTANT
The author's intention in this GitHub issue is likely related to an "enhancement" because the title "Remove search term tab groups from History" suggests a request for a feature change or improvement in the functionality of the app, rather than reporting an unexpected problem or unintended behavior.

turns-00032.parquet:9366

fd020b7bcecbbbf1ee49fb11
turn 1/1gpt-4o-2024-08-06EnglishUnited States121 words
degenerate_repetitionAbsentFinal dense release
USER
Assistant: 
User: System: I will provide you with a textual GitHub issue title of an Android app, aiming to categorize the issues into one or multiple specific intention classes:
bug: indicates an unexpected problem or unintended behavior
enhancement: Indicates new feature requests
other: Other categories
User: 'prompt:I will provide you with a textual GitHub issue title of an Android app, aiming to categorize the issues into one or multiple specific intention classes:
bug: indicates an unexpected problem or unintended behavior
enhancement: Indicates new feature requests
other: Other categories
Does the author's intention in this GitHub issue relate to a bug or enhancement or other?
issue: Amazon Firestick: Native Out of Memory error while playing 4k '
intention: '
Assistant:
Assistant:
ASSISTANT
bug

turns-00032.parquet:9367

58d62f01ee941d379880d6bd
turn 1/1gpt-4o-2024-08-06EnglishIreland120 words
degenerate_repetitionAbsentFinal dense release
USER
Assistant: 
User: System: I will provide you with a textual GitHub issue title of an Android app, aiming to categorize the issues into one or multiple specific intention classes:
bug: indicates an unexpected problem or unintended behavior
enhancement: Indicates new feature requests
other: Other categories
User: 'prompt:I will provide you with a textual GitHub issue title of an Android app, aiming to categorize the issues into one or multiple specific intention classes:
bug: indicates an unexpected problem or unintended behavior
enhancement: Indicates new feature requests
other: Other categories
Does the author's intention in this GitHub issue relate to a bug or enhancement or other?
issue: Error: Failed to extract file error in opening zip file'
intention: '
Assistant:
Assistant:
ASSISTANT
bug

turns-00032.parquet:9368

3101444ef38600150a951cee
turn 1/1gpt-4o-2024-08-06EnglishUnited States113 words
degenerate_repetitionAbsentFinal dense release
USER
Assistant: 
User: System: I will provide you with a textual GitHub issue title of an Android app, aiming to categorize the issues into one or multiple specific intention classes:
bug: indicates an unexpected problem or unintended behavior
enhancement: Indicates new feature requests
other: Other categories
User: 'prompt:I will provide you with a textual GitHub issue title of an Android app, aiming to categorize the issues into one or multiple specific intention classes:
bug: indicates an unexpected problem or unintended behavior
enhancement: Indicates new feature requests
other: Other categories
Does the author's intention in this GitHub issue relate to a bug or enhancement or other?
issue: Feature Request: GIF'
intention: '
Assistant:
Assistant:
ASSISTANT
enhancement

turns-00032.parquet:9369

e9ca0e2922c7118d5ddbc275
turn 1/1gpt-4o-mini-2024-07-18SpanishUnited States42 words
degenerate_repetitionAbsentFinal dense release
USER
Mezcla el nombre de usuario "Lau" con el nombre de usuario "dios", MUY BIEN MEZCLADOS LITERALMENTE UNE LOS DOS NOMBRES EN UNO SOLO y solo dime el resultado de la mezcla, NI UNA PALABRA MAS, SOLO EL RESULTADO DE LA MEZCLA.
ASSISTANT
Laudios

turns-00032.parquet:9370

26e4a041a9999c13fa179261
turn 1/1gpt-4o-2024-08-06EnglishUnited States159 words
degenerate_repetitionAbsentFinal dense release
USER
Assistant: 
User: System: I will provide you with a textual GitHub issue title of an Android app, aiming to categorize the issues into one or multiple specific intention classes:
bug: indicates an unexpected problem or unintended behavior
enhancement: Indicates new feature requests
other: Other categories
User: 'prompt:I will provide you with a textual GitHub issue title of an Android app, aiming to categorize the issues into one or multiple specific intention classes:
bug: indicates an unexpected problem or unintended behavior
enhancement: Indicates new feature requests
other: Other categories
Does the author's intention in this GitHub issue relate to a bug or enhancement or other?
issue: Convert CustomViewPager to Kotlin'
intention: '
Assistant:
Assistant:
ASSISTANT
The issue title "Convert CustomViewPager to Kotlin" relates to an enhancement. Converting code from Java to Kotlin is often done to improve code readability, maintainability, or to take advantage of Kotlin's features, which typically classifies it as an enhancement rather than a bug or other category.

turns-00032.parquet:9371

677f541aa59dfcb25e223d22
turn 1/1gpt-4o-2024-08-06EnglishUnited Kingdom119 words
degenerate_repetitionAbsentFinal dense release
USER
Assistant: 
User: System: I will provide you with a textual GitHub issue title of an Android app, aiming to categorize the issues into one or multiple specific intention classes:
bug: indicates an unexpected problem or unintended behavior
enhancement: Indicates new feature requests
other: Other categories
User: 'prompt:I will provide you with a textual GitHub issue title of an Android app, aiming to categorize the issues into one or multiple specific intention classes:
bug: indicates an unexpected problem or unintended behavior
enhancement: Indicates new feature requests
other: Other categories
Does the author's intention in this GitHub issue relate to a bug or enhancement or other?
issue: Browser shows grey screen for a while, then crashes'
intention: '
Assistant:
Assistant:
ASSISTANT
bug

turns-00032.parquet:9372

321d9773755960e20ec4e42a
turn 1/7gpt-4o-2024-08-06RussianUnited Kingdom5233 words
degenerate_repetitionAbsentFinal dense release
USER
так, смотри, у меня есть телеграм бот для работы с сервисом по покупке сотовыйх номеров и получения смс на них, мой бот работает и вроде все в порядке, но бот слишком сложно у меня структурирован, я хочу, чтобы ты помог мне улучшить код бота, но тчобы он раотал точно так же, без ошибок и проблем, возможно это ускорит его работу и оптимирзурет, возможно ты что-то сможешь оптимизирвоат, в общем я тебе присылаю все файлы бота, а ты передалй их, как можно лучше и пришли мне готовый резульатт. так же я тебе отправлю документацию по работе с апи сервисом, может ты найдешь какие-то ошбики,

1. /config.py
import os
from dotenv import load_dotenv

load_dotenv()

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

2. /main.py
# main.py
import asyncio
import logging
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from bot.handlers import router, on_startup  # Импортируйте роутер и on_startup

API_TOKEN = "7625597085:AAGfqRVnE75JYUEdiI045jAKzd8jJ-ZiHLI"  # Замените на ваш токен

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

async def main():
    # Инициализация бота и диспетчера
    bot = Bot(token=API_TOKEN)
    storage = MemoryStorage()
    dp = Dispatcher(storage=storage)

    # Регистрация роутера
    dp.include_router(router)

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

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

if __name__ == "__main__":
    asyncio.run(main())

3. /bot/api.py
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")
        async with aiohttp.ClientSession() as session:
            async with session.get(BASE_URL, params={
                "api_key": self.api_key,
                "action": "getBalance",
                "currency": "643"
            }) as response:
                if response.status == 200:
                    text = await response.text()
                    logging.info(f"Ответ баланса: {text}")
                    return text
                logging.error(f"Ошибка при запросе баланса: {response.status}")
                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:
                if response.status == 200:
                    text = await response.text()
                    logging.info(f"Ответ номера: {text}")
                    return text
                logging.error(f"Ошибка при запросе номера: {response.status}")
                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:
                if response.status == 200:
                    response_text = await 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}'.")
                            return None
                        return service_data  # Возвращаем словарь { "Цена": Количество }
                    except json.JSONDecodeError:
                        # Обрабатываем случаи, когда ответ не является JSON (например, ошибки от API)
                        logging.error(f"Ошибка от API при получении цен: {response_text}")
                        return None
                else:
                    logging.error(f"Ошибка при запросе цен: {response.status}")
                    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 and operator != "any":
            params["operator"] = operator
        async with aiohttp.ClientSession() as session:
            async with session.get(BASE_URL, params=params) as response:
                if response.status == 200:
                    response_text = await response.text()
                    try:
                        numbers_status = json.loads(response_text)
                        logging.info(f"Статус номеров: {numbers_status}")
                        return numbers_status  # Возвращает словарь { "SERVICE_CODE": Количество }
                    except json.JSONDecodeError:
                        logging.error(f"Ошибка при получении статуса номеров: {response_text}")
                        return None
                else:
                    logging.error(f"Ошибка при запросе статуса номеров: {response.status}")
                    return None

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

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

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

4. /bot/data.py
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} нет оператора для удаления.")


5. /bot/handlers.py
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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

### Оптимизации и рекомендации:

1. **Удаление лишних зависимостей и улучшение импорта:**
   - Перенесите импорт `aiogram` и других библиотек, которые используются в нескольких модулях, в отдельную секцию для облегчения управления зависимостями.
   - Рассмотрите использование `pipenv`, `poetry` или `requirements.txt` для управления зависимостями.

2. **Параметры конфигурации:**
   - Хранить все ваши конфигурационные параметры (например, токены, зрительские ключи и ID) в файле конфигурации `.env`. Это улучшает безопасность и упрощает управление конфигурацией. Вы уже используете `dotenv`, но не извлекаете все параметры из `.env`.

3. **Более эффективное использование aiohttp:**
   - Рассмотрите изменение класса `SmsHubAPI`, чтобы один объект `aiohttp.ClientSession` был создан и использован во всех методах. Это поможет избежать лишней инициализации сеансов HTTP, которая может замедлять выполнение.

4. **Кэширование и управление состоянием:**
   - В данных, которые необходимо часто обновлять, таких как курсы валют или статусы активаций, используйте более эффективные структуры, например Redis, для хранения временных данных, где потенциально вы можете часто обращаться к диску.

5. **Логирование:**
   - Настройте логирование через `logging.config` или используйте внешние сервисы для централизованного управления логами, такие как Sentry.

6. **Работа с API:**
   - Проверьте, везде ли корректно обрабатываются ошибки от API и корректно ли используются параметры валюты в запросах.

### Общая структура:

Я рекомендую пересмотреть структуру вашего проекта для организации кода следующим образом:

```
/bot_project
  /bot
    __init__.py
    api.py
    data.py
    handlers.py
  /config
    __init__.py
    settings.py  # для всех глобальных настроек
  /utils
    __init__.py
    logger.py  # настройка логирования
  main.py
  .env
  requirements.txt
```

### Обновлённый пример:

Применяя все вышеуказанные рекомендации:

### config/settings.py
```python
import os
from dotenv import load_dotenv

load_dotenv()

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

### main.py
```python
import asyncio
import logging
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from bot.handlers import router, on_startup
from config import settings

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)

async def main():
    bot = Bot(token=settings.TELEGRAM_TOKEN)
    storage = MemoryStorage()
    dp = Dispatcher(storage=storage)
    dp.include_router(router)
    dp.startup.register(on_startup)

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

if __name__ == "__main__":
    asyncio.run(main())
```

### bot/api.py
(Настройте и используйте один HTTP-клиент для всех запросов)

```python
import aiohttp

class SmsHubAPI:
    BASE_URL = "https://smshub.org/stubs/handler_api.php"
    
    def __init__(self, api_key):
        self.api_key = api_key
        self.session = None
        
    async def __aenter__(self):
        self.session = aiohttp.ClientSession()
        return self
        
    async def __aexit__(self, exc_type, exc, tb):
        await self.session.close()
        
    async def request(self, **params):
        params['api_key'] = self.api_key
        async with self.session.get(self.BASE_URL, params=params) as response:
            if response.status == 200:
                return await response.text()
            return None

    async def get_balance(self, currency='643'):
        return await self.request(action='getBalance', currency=currency)
    
    async def get_number(self, service, country='0', operator=None, max_price=None, currency='643'):
        params = {'action': 'getNumber', 'service': service, 'country': country, 'currency': currency}
        if operator and operator != "any":
            params["operator"] = operator
        if max_price:
            params["maxPrice"] = max_price
        return await self.request(**params)

    # Остальные методы аналогично
```

Этот принцип можно расширить на все остальные функции и классы.

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

turns-00032.parquet:9373

30edc54eb38b49de51c1d524
turn 2/7gpt-4o-2024-08-06RussianUnited Kingdom197 words
degenerate_repetitionAbsentFinal dense release
USER
так, у меня был запущен телеграм бот через systemctl, сейчас нужнот сотанвить прцоесс и вруную чрез консоль азпустить, мои действия
ASSISTANT
Чтобы остановить и вручную запустить Telegram-бота, вам нужно выполнить следующие шаги:

### 1. Остановить бота, запущенного через systemctl
Сначала вам нужно остановить службу, которая управляет вашим ботом. Предположим, что ваш бот запускается под именем `telegram_bot.service`. Выполните следующую команду:

```bash
sudo systemctl stop telegram_bot.service
```

Эта команда остановит службу, и ваш бот перестанет работать.

### 2. Запустить бота вручную через консоль
Теперь, когда служба остановлена, вы можете запустить бота вручную. Предположим, что у вас есть скрипт, запускающий бота, например `main.py`. Для запуска бота вручную выполните в консоли:

```bash
python3 path/to/main.py
```

где `path/to/main.py` — это путь к скрипту вашего бота.

### Важно:
- Убедитесь, что у вас активирована виртуальная среда (если она используется) перед запуском скрипта вручную.
- Убедитесь, что все зависимости и переменные окружения правильно настроены в вашей консоли.

### Примечание
При запуске вручную бот будет работать только пока открыт терминал. Если вы закроете терминал, где запущен бот, процесс остановится. Для постоянной работы рекомендуется запускать бота в фоновом режиме с использованием инструментов, таких как `tmux`, `screen` или аналогичных, и использовать `systemctl` для управления службой в долгосрочной перспективе.