USER
я тебе присылаю полный код моего бота телеграм, я хочу улучшить код, чтобы он был более структурированный и правильный, без ошибок
документация: Внимание! Добавлена поддержка валюты.
Методы 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¤cy=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¤cy=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¤cy=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&¤cy=CURRENCY
Входные параметры
Параметр Принимаемые значения Обязательность Описание
APIKEY APIKEY Да Ваш API-ключ, обеспечивает доступ к оплаченным услугам. API-ключ следует хранить в тайне
CURRENCY 643, 840 Да Значение валюты которое будет установлено по умолчанию для запросов по API.
Выходные данные
Ответ сервера Описание
JSON ответ Ответ в формате {"status":"success"}
Возможные ошибки
Ответ сервера Описание
BAD_CURRENCY Неверное значение currency
CURRENCY_CHANGE_UNAVAILABLE Изменение валюты недоступно
# requirements!.txt
python-dotenv
aiogram
requests
# main.py
import asyncio
import logging
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from config import TELEGRAM_TOKEN
from bot.handlers import register_handlers
from bot.utils import on_startup
from aiogram.client.bot import DefaultBotProperties
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
async def main():
# Инициализация бота и диспетчера
bot = Bot(token=TELEGRAM_TOKEN, default=DefaultBotProperties(parse_mode='HTML'))
storage = MemoryStorage()
dp = Dispatcher(storage=storage)
# Регистрация обработчиков
register_handlers(dp)
# Регистрация функции on_startup
dp.startup.register(on_startup)
try:
logger.info("Запуск бота...")
await dp.start_polling(bot)
finally:
await bot.close()
if __name__ == "__main__":
asyncio.run(main())
# config.py
import os
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("API_KEY")
TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
ADMIN_CHAT_ID = int(os.getenv("ADMIN_CHAT_ID", "389669884"))
# bot/utils.py
import logging
import time
import aiohttp
from aiogram import Bot, Dispatcher, types
from aiogram.fsm.context import FSMContext
from typing import Optional
logger = logging.getLogger(__name__)
EXCHANGE_RATE_API_URL = "https://api.exchangerate-api.com/v4/latest/USD"
USD_TO_RUB_RATE_TTL = 3600 # Кэшируем курс на 1 час
_cached_usd_to_rub_rate: Optional[float] = None
_cached_usd_to_rub_rate_time: float = 0.0
async def get_usd_to_rub_rate() -> Optional[float]:
"""Получить текущий курс USD к RUB с кэшированием."""
global _cached_usd_to_rub_rate, _cached_usd_to_rub_rate_time
now = time.time()
if _cached_usd_to_rub_rate and (now - _cached_usd_to_rub_rate_time < USD_TO_RUB_RATE_TTL):
return _cached_usd_to_rub_rate
try:
async with aiohttp.ClientSession() as session:
async with session.get(EXCHANGE_RATE_API_URL) as response:
data = await response.json()
rate = data.get('rates', {}).get('RUB')
if rate:
_cached_usd_to_rub_rate = rate
_cached_usd_to_rub_rate_time = now
return rate
else:
logger.error("Курс RUB не найден в ответе.")
return None
except Exception as e:
logger.error(f"Ошибка при запросе курса валют: {e}")
return None
async def safe_edit_or_send_message(message: types.Message, new_text: str, state: FSMContext, reply_markup=None, parse_mode=None):
"""Безопасное редактирование или отправка нового сообщения."""
try:
await message.edit_text(new_text, reply_markup=reply_markup, parse_mode=parse_mode)
await state.update_data(last_message_id=message.message_id)
except Exception as e:
logger.error(f"Failed to edit message: {e}")
data = await state.get_data()
last_message_id = data.get('last_message_id')
if last_message_id:
try:
await message.bot.delete_message(chat_id=message.chat.id, message_id=last_message_id)
except Exception as del_exc:
logger.error(f"Failed to delete message: {del_exc}")
sent_message = await message.answer(new_text, reply_markup=reply_markup, parse_mode=parse_mode)
await state.update_data(last_message_id=sent_message.message_id)
async def safe_delete_message(bot: Bot, chat_id: int, message_id: int) -> None:
"""Безопасное удаление сообщения."""
try:
await bot.delete_message(chat_id=chat_id, message_id=message_id)
except Exception as e:
logger.error(f"Failed to delete message: {e}")
async def on_startup(dispatcher: Dispatcher):
"""Функция, выполняемая при старте бота."""
logger.info("Бот успешно запущен.")
# Здесь можно добавить запуск фоновых задач, если необходимо
# bot/api.py
import aiohttp
import logging
import json
from typing import Optional, Dict
logger = logging.getLogger(__name__)
BASE_URL = "https://smshub.org/stubs/handler_api.php"
class SmsHubAPI:
def __init__(self, api_key: str):
self.api_key = api_key
async def get_balance(self) -> Optional[str]:
"""Запрос балансввыа с заданной валютой (RUB)."""
logger.info("Запрос баланса с валютой: 643")
params = {
"api_key": self.api_key,
"action": "getBalance",
"currency": "643"
}
async with aiohttp.ClientSession() as session:
async with session.get(BASE_URL, params=params) as response:
text = await response.text()
if response.status == 200:
logger.info(f"Ответ баланса: {text}")
return text
logger.error(f"Ошибка при запросе баланса: {response.status} - {text}")
return None
async def get_number(self, service: str, country: str = "0", operator: Optional[str] = None, max_price: Optional[float] = None) -> Optional[str]:
"""Получение номераа для указанного сервиса и страны."""
logger.info(f"Запрос номера для сервиса: {service}, страна: {country}, оператор: {operator}, макс. цена: {max_price}, валюта: 643")
params = {
"api_key": self.api_key,
"action": "getNumber",
"service": service,
"country": country,
"currency": "643"
}
if operator and operator != "any":
params["operator"] = operator
if max_price:
params["maxPrice"] = max_price
async with aiohttp.ClientSession() as session:
async with session.get(BASE_URL, params=params) as response:
text = await response.text()
if response.status == 200:
logger.info(f"Ответ номера: {text}")
return text
logger.error(f"Ошибка при запросе номера: {response.status} - {text}")
return None
async def get_price_for_service(self, service: str, country: str = "0") -> Optional[Dict]:
"""Получение цен для указанного сервиса."""
logger.info(f"Запрос цен для сервиса: {service}, страна: {country}, валюта: 643")
params = {
"api_key": self.api_key,
"action": "getPrices",
"service": service,
"country": country,
"currency": "643"
}
async with aiohttp.ClientSession() as session:
async with session.get(BASE_URL, params=params) as response:
response_text = await response.text()
if response.status == 200:
try:
prices_data = json.loads(response_text)
logger.info(f"Цены для сервиса {service}: {prices_data}")
country_data = prices_data.get(country)
if not country_data:
logger.error(f"Страна '{country}' отсутствует в данных: {prices_data}")
return None
service_data = country_data.get(service)
if not service_data:
logger.error(f"Сервис '{service}' отсутствует среди доступных для страны '{country}'. Данные: {country_data}")
return None
return service_data # Возвращает словарь { "Цена": Количество }
except json.JSONDecodeError:
logger.error(f"Ошибка парсинга JSON: {response_text}")
return None
else:
logger.error(f"Ошибка при запросе цен: {response.status} - {response_text}")
return None
async def get_numbers_status(self, country: str = "0", operator: Optional[str] = None) -> Optional[Dict]:
"""Получение статуса номеров по стране и оператору."""
logger.info(f"Запрос статуса номеров для страны: {country}, оператор: {operator}")
params = {
"api_key": self.api_key,
"action": "getNumbersStatus",
"country": country
}
if operator and operator != "any":
params["operator"] = operator
async with aiohttp.ClientSession() as session:
async with session.get(BASE_URL, params=params) as response:
response_text = await response.text()
if response.status == 200:
try:
numbers_status = json.loads(response_text)
logger.info(f"Статус номеров: {numbers_status}")
return numbers_status # Возвращает словарь { "SERVICE_CODE": Количество }
except json.JSONDecodeError:
logger.error(f"Ошибка парсинга JSON: {response_text}")
return None
else:
logger.error(f"Ошибка при запросе статуса номеров: {response.status} - {response_text}")
return None
async def get_status(self, activation_id: str) -> Optional[str]:
"""Получение статуса активации по ID."""
logger.info(f"Запрос статуса для активации: {activation_id}")
params = {
"api_key": self.api_key,
"action": "getStatus",
"id": activation_id
}
async with aiohttp.ClientSession() as session:
async with session.get(BASE_URL, params=params) as response:
text = await response.text()
if response.status == 200:
logger.info(f"Ответ статуса: {text}")
return text
logger.error(f"Ошибка при запросе статуса: {response.status} - {text}")
return None
async def set_status(self, activation_id: str, status: str) -> Optional[str]:
"""Установка статуса для активации."""
logger.info(f"Изменение статуса активации: {activation_id} на статус {status}")
params = {
"api_key": self.api_key,
"action": "setStatus",
"id": activation_id,
"status": status
}
async with aiohttp.ClientSession() as session:
async with session.get(BASE_URL, params=params) as response:
text = await response.text()
if response.status == 200:
logger.info(f"Ответ изменения статуса: {text}")
return text
logger.error(f"Ошибка при изменении статуса: {response.status} - {text}")
return None
async def cancel_activation(self, activation_id: str) -> Optional[str]:
"""Отмена активации."""
return await self.set_status(activation_id, status="8")
# bot/data.py
import json
import os
import threading
import logging
from typing import Optional, Dict, Any
logger = logging.getLogger(__name__)
# Путь к файлу для хранения данных пользователей
DATA_FILE = os.path.join(os.path.dirname(__file__), "data.json")
lock = threading.RLock()
# Загружаем данные из файла при запуске
user_data: Dict[str, Any] = {}
def load_data() -> None:
"""Загружает данные из JSON файла."""
global user_data
with lock:
if os.path.exists(DATA_FILE):
try:
with open(DATA_FILE, "r", encoding="utf-8") as file:
user_data = json.load(file)
logger.info("Данные пользователей успешно загружены.")
except json.JSONDecodeError:
user_data = {}
logger.warning("Не удалось декодировать JSON из файла. Используем пустые данные.")
else:
user_data = {}
logger.info("Файл данных не найден. Используем пустые данные.")
# Вызываем load_data() при импорте модуля
load_data()
def save_data() -> None:
"""Сохраняет текущие данные в JSON файл."""
with lock:
with open(DATA_FILE, "w", encoding="utf-8") as file:
json.dump(user_data, file, ensure_ascii=False, indent=4)
logger.info("Данные пользователей успешно сохранены.")
# Методы для работы с API ключами
def set_user_api_key(user_id: int, api_key: str) -> None:
"""Сохраняет API ключ для конкретного пользователя."""
with lock:
user = user_data.get(str(user_id), {})
user["api_key"] = api_key
user_data[str(user_id)] = user
save_data()
logger.info(f"API ключ для пользователя {user_id} сохранён.")
def get_user_api_key(user_id: int) -> Optional[str]:
"""Возвращает API ключ для конкретного пользователя, если он сохранен."""
with lock:
return user_data.get(str(user_id), {}).get("api_key")
def delete_user_api_key(user_id: int) -> None:
"""Удаляет API ключ для конкретного пользователя."""
with lock:
user = user_data.get(str(user_id), {})
if "api_key" in user:
del user["api_key"]
if not user:
del user_data[str(user_id)]
save_data()
logger.info(f"API ключ для пользователя {user_id} удалён.")
else:
logger.warning(f"У пользователя {user_id} нет API ключа для удаления.")
# Методы для работы с оператором
def set_user_operator(user_id: int, operator: str) -> None:
"""Сохраняет выбранный оператор для конкретного пользователя."""
with lock:
user = user_data.get(str(user_id), {})
user["operator"] = operator
user_data[str(user_id)] = user
save_data()
logger.info(f"Оператор '{operator}' для пользователя {user_id} сохранён.")
def get_user_operator(user_id: int) -> Optional[str]:
"""Возвращает выбранный оператор для конкретного пользователя, если он сохранен."""
with lock:
return user_data.get(str(user_id), {}).get("operator")
def delete_user_operator(user_id: int) -> None:
"""Удаляет выбранный оператор для конкретного пользователя."""
with lock:
user = user_data.get(str(user_id), {})
if "operator" in user:
del user["operator"]
if not user:
del user_data[str(user_id)]
save_data()
logger.info(f"Оператор для пользователя {user_id} удалён.")
else:
logger.warning(f"У пользователя {user_id} нет оператора для удаления.")
# Методы для работы со страной
def set_user_country(user_id: int, country: str) -> None:
"""Сохраняет выбранную страну для конкретного пользователя."""
with lock:
user = user_data.get(str(user_id), {})
user["country"] = country
user_data[str(user_id)] = user
save_data()
logger.info(f"Страна '{country}' для пользователя {user_id} сохранена.")
def get_user_country(user_id: int) -> Optional[str]:
"""Возвращает выбранную страну для конкретного пользователя, если она сохранена."""
with lock:
return user_data.get(str(user_id), {}).get("country")
def delete_user_country(user_id: int) -> None:
"""Удаляет выбранную страну для конкретного пользователя."""
with lock:
user = user_data.get(str(user_id), {})
if "country" in user:
del user["country"]
if not user:
del user_data[str(user_id)]
save_data()
logger.info(f"Страна для пользователя {user_id} удалена.")
else:
logger.warning(f"У пользователя {user_id} нет страны для удаления.")
# bot/handlers/__init__.py
from aiogram import Dispatcher
from .start import register_start_handlers
from .settings import register_settings_handlers
from .balance import register_balance_handlers
from .sms import register_sms_handlers
from .activation import register_activation_handlers
from .errors import register_error_handlers
def register_handlers(dp: Dispatcher):
register_start_handlers(dp)
register_settings_handlers(dp)
register_balance_handlers(dp)
register_sms_handlers(dp)
register_activation_handlers(dp)
register_error_handlers(dp)
# bot/handlers/activation.py
import asyncio
import logging
import time
from typing import Dict, Any
from aiogram import Dispatcher, types, F, Bot
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from ..data import get_user_api_key
from ..api import SmsHubAPI
from ..utils import safe_edit_or_send_message, safe_delete_message
from .main_menu import send_main_menu
logger = logging.getLogger(__name__)
user_activations: Dict[int, Dict[str, Dict[str, Any]]] = {}
async def current_activations_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
user_id = callback_query.from_user.id
activations = user_activations.get(user_id, {})
keyboard = InlineKeyboardBuilder()
if not activations:
keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=callback_query.message,
new_text="У вас нет активных активаций.",
state=state,
reply_markup=keyboard.as_markup()
)
return
for activation_id, data in activations.items():
keyboard.button(
text=f"{data['service']} ({data['number']}) - {data['status']}",
callback_data=f"manage_activation:{activation_id}"
)
keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Ваши текущие активации:",
state=state,
reply_markup=keyboard.as_markup()
)
async def manage_activation_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
user_id = callback_query.from_user.id
activation_id = callback_query.data.split(":")[1]
activations = user_activations.get(user_id, {})
activation = activations.get(activation_id)
if not activation:
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Активация не найдена.",
state=state,
reply_markup=callback_query.message.reply_markup
)
return
keyboard = InlineKeyboardBuilder()
# Формируем клавиатуру в зависимости от статуса активации
if activation['status'] == 'Ожидание SMS':
# Только отменить активацию
keyboard.button(text="❌ Отменить активацию", callback_data=f"cancel_activation:{activation_id}")
elif activation['status'].startswith('Код получен'):
# Запросить еще SMS или завершить активацию
keyboard.button(text="🔄 Запросить еще SMS", callback_data=f"request_another_sms:{activation_id}")
keyboard.button(text="✅ Завершить активацию", callback_data=f"complete_activation:{activation_id}")
elif activation['status'].startswith('Ожидание повторного SMS'):
# Завершить или отменить активацию
keyboard.button(text="✅ Завершить активацию", callback_data=f"complete_activation:{activation_id}")
keyboard.button(text="❌ Отменить активацию", callback_data=f"cancel_activation:{activation_id}")
else:
# Другие статусы
keyboard.button(text="✅ Завершить активацию", callback_data=f"complete_activation:{activation_id}")
keyboard.button(text="❌ Отменить активацию", callback_data=f"cancel_activation:{activation_id}")
keyboard.button(text="🔙 Назад", callback_data="current_activations")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=callback_query.message,
new_text=(
f"Активация {activation_id}:\n"
f"Сервис: {activation['service']}\n"
f"Номер: `{activation['number']}`\n"
f"Статус: {activation['status']}"
),
state=state,
reply_markup=keyboard.as_markup(),
parse_mode="Markdown"
)
async def cancel_activation_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
user_id = callback_query.from_user.id
activation_id = callback_query.data.split(":")[1]
user_api_key = get_user_api_key(user_id)
smshub_api = SmsHubAPI(user_api_key)
activations = user_activations.get(user_id, {})
activation = activations.get(activation_id)
if activation:
# Отменяем задачу, если она есть
task = activation.get('task')
if task:
task.cancel()
# Отменяем активацию на стороне API
result = await smshub_api.cancel_activation(activation_id)
logger.info(f"Активация {activation_id} отменена на стороне API: {result}")
# Удаляем активацию из списка
del activations[activation_id]
# Создаём новую клавиатуру с кнопкой "🔙 Назад"
keyboard = InlineKeyboardBuilder()
keyboard.button(text="🔙 Назад", callback_data="current_activations")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Активация успешно отменена.",
state=state,
reply_markup=keyboard.as_markup()
)
else:
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Активация не найдена.",
state=state,
reply_markup=callback_query.message.reply_markup
)
async def request_another_sms_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
user_id = callback_query.from_user.id
activation_id = callback_query.data.split(":")[1]
user_api_key = get_user_api_key(user_id)
smshub_api = SmsHubAPI(user_api_key)
activations = user_activations.get(user_id, {})
activation = activations.get(activation_id)
if not activation:
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Активация не найдена.",
state=state,
reply_markup=callback_query.message.reply_markup
)
return
result = await smshub_api.set_status(activation_id, status="3")
if result == "ACCESS_RETRY_GET":
# Отменяем предыдущую задачу, если она есть
task = activation.get('task')
if task:
task.cancel()
# Запускаем новую задачу
task = asyncio.create_task(poll_for_sms(
bot=callback_query.message.bot,
user_id=user_id,
chat_id=callback_query.from_user.id,
activation_id=activation_id,
smshub_api=smshub_api
))
activation['task'] = task
activation['status'] = 'Ожидание повторного SMS'
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Запрошен повторный SMS. Ожидание SMS...",
state=state,
reply_markup=callback_query.message.reply_markup
)
else:
logger.error(f"Не удалось запросить повторное SMS. Ответ: {result}")
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Не удалось запросить повторный SMS. Попробуйте снова.",
state=state,
reply_markup=callback_query.message.reply_markup
)
async def complete_activation_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
user_id = callback_query.from_user.id
activation_id = callback_query.data.split(":")[1]
user_api_key = get_user_api_key(user_id)
smshub_api = SmsHubAPI(user_api_key)
activations = user_activations.get(user_id, {})
activation = activations.get(activation_id)
if not activation:
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Активация не найдена.",
state=state,
reply_markup=callback_query.message.reply_markup
)
return
result = await smshub_api.set_status(activation_id, status="6")
if result == "ACCESS_ACTIVATION":
# Отменяем задачу, если она есть
task = activation.get('task')
if task:
task.cancel()
# Удаляем активацию из списка
del activations[activation_id]
# Создаём новую клавиатуру с кнопкой "🔙 Назад"
keyboard = InlineKeyboardBuilder()
keyboard.button(text="🔙 Назад", callback_data="current_activations")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Активация успешно завершена.",
state=state,
reply_markup=keyboard.as_markup()
)
else:
logger.error(f"Не удалось завершить активацию. Ответ: {result}")
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Не удалось завершить активацию. Попробуйте снова.",
state=state,
reply_markup=callback_query.message.reply_markup
)
async def poll_for_sms(bot: Bot, user_id: int, chat_id: int, activation_id: str, smshub_api: SmsHubAPI):
activations = user_activations.get(user_id, {})
activation = activations.get(activation_id)
if not activation:
logger.error(f"Активация {activation_id} не найдена для пользователя {user_id}.")
return
try:
max_wait_time = 300 # Максимальное время ожидания в секундах
poll_interval = 3 # Интервал опроса в секундах
elapsed_time = 0
while elapsed_time < max_wait_time:
await asyncio.sleep(poll_interval)
elapsed_time += poll_interval
status_response = await smshub_api.get_status(activation_id)
if not status_response:
logger.error("Нет ответа при запросе статуса активации.")
await bot.send_message(chat_id, "Не удалось получить статус активации. Попробуйте позже.")
return
status_parts = status_response.split(":", 1)
status = status_parts[0]
if status == "STATUS_WAIT_CODE":
logger.debug(f"Ожидание SMS кода для активации {activation_id}.")
activation['last_update'] = time.time()
continue
elif status.startswith("STATUS_WAIT_RETRY"):
last_code = status_parts[1] if len(status_parts) > 1 else "нет предыдущего кода"
logger.info(f"Ожидание повторного SMS кода, последний код: {last_code}")
activation['status'] = f'Ожидание повторного SMS (последний код: {last_code})'
activation['last_update'] = time.time()
continue
elif status == "STATUS_CANCEL":
await bot.send_message(chat_id, "Активация была отменена.")
activation['status'] = 'Активация отменена'
return
elif status == "STATUS_OK":
code = status_parts[1] if len(status_parts) > 1 else "Код не получен"
logger.info(f"Получен код активации для {activation_id}: {code}")
activation['status'] = f'Код получен: {code}'
activation['last_update'] = time.time()
await bot.send_message(
chat_id,
f"Код активации для номера `{activation['number']}`: `{code}`",
parse_mode="Markdown"
)
return
else:
logger.error(f"Непредвиденный статус активации: {status_response}")
activation['status'] = f'Неизвестный статус: {status}'
await bot.send_message(chat_id, f"Неизвестный статус активации: {status}")
return
# Если время ожидания истекло, функция завершается без изменения статуса активации
except asyncio.CancelledError:
logger.info(f"Задача poll_for_sms для активации {activation_id} была отменена.")
except Exception as e:
logger.error(f"Ошибка в poll_for_sms: {e}")
activation['status'] = 'Ошибка при получении SMS'
await bot.send_message(chat_id, "Произошла ошибка при получении SMS.")
def register_activation_handlers(dp: Dispatcher):
dp.callback_query.register(current_activations_handler, F.data == "current_activations")
dp.callback_query.register(manage_activation_handler, F.data.startswith("manage_activation"))
dp.callback_query.register(cancel_activation_handler, F.data.startswith("cancel_activation"))
dp.callback_query.register(request_another_sms_handler, F.data.startswith("request_another_sms"))
dp.callback_query.register(complete_activation_handler, F.data.startswith("complete_activation"))
# bot/handlers/balance.py
import logging
from aiogram import Dispatcher, types, F
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from datetime import datetime
from ..data import get_user_api_key
from ..api import SmsHubAPI
from ..utils import get_usd_to_rub_rate, safe_edit_or_send_message
logger = logging.getLogger(__name__)
async def balance_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
user_api_key = get_user_api_key(callback_query.from_user.id)
if not user_api_key:
keyboard = InlineKeyboardBuilder()
keyboard.button(text="⚙️ Настройки", callback_data="settings")
keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Пожалуйста, сначала настройте ваш API ключ в разделе 'Настройки'.",
state=state,
reply_markup=keyboard.as_markup()
)
return
smshub_api = SmsHubAPI(user_api_key)
logger.info(f"Пользователь {callback_query.from_user.id} запросил баланс.")
balance_response = await smshub_api.get_balance()
usd_to_rub_rate = await get_usd_to_rub_rate()
if balance_response and balance_response.startswith("ACCESS_BALANCE:"):
balance_usd = float(balance_response.split(":")[1])
balance_rub = balance_usd * usd_to_rub_rate if usd_to_rub_rate else "недоступно"
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
message_text = (
f"Ваш баланс на {current_time}:\n"
f"`{balance_usd:.4f}`$ (приблизительно `{balance_rub:.2f}`₽)"
)
keyboard = InlineKeyboardBuilder()
keyboard.button(text="🔄 Обновить баланс", callback_data="check_balance")
keyboard.button(text="🔙 Вернуться в меню", callback_data="back_to_main_menu")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=callback_query.message,
new_text=message_text,
state=state,
reply_markup=keyboard.as_markup(),
parse_mode="Markdown"
)
else:
logger.error(f"Не удалось получить баланс. Ответ: {balance_response}")
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Не удалось получить баланс. Попробуйте снова.",
state=state,
reply_markup=callback_query.message.reply_markup
)
def register_balance_handlers(dp: Dispatcher):
dp.callback_query.register(balance_handler, F.data == "check_balance")
# bot/handlers/errors.py
import logging
from aiogram import Dispatcher, types
logger = logging.getLogger(__name__)
async def errors_handler(update: types.Update, exception: Exception):
logger.exception(f"Exception in update handler: {exception}")
return True
def register_error_handlers(dp: Dispatcher):
dp.errors.register(errors_handler)
# bot/handlers/main_menu.py
import logging
from aiogram import types
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from ..utils import safe_edit_or_send_message # Импортируем функцию
logger = logging.getLogger(__name__)
async def send_main_menu(message: types.Message, state: FSMContext):
logger.info(f"Пользователь {message.chat.id} вернулся в главное меню.")
keyboard = InlineKeyboardBuilder()
keyboard.button(text="💰 Проверить баланс", callback_data="check_balance")
keyboard.button(text="📲 Получить SMS", callback_data="get_sms")
keyboard.button(text="📋 Текущие активации", callback_data="current_activations")
keyboard.button(text="⚙️ Настройки", callback_data="settings")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=message,
new_text="Добро пожаловать! Выберите действие:",
state=state,
reply_markup=keyboard.as_markup()
)
# bot/handlers/settings.py
import logging
from aiogram import Dispatcher, types, F
from aiogram.fsm.context import FSMContext
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.state import State, StatesGroup
import os
from ..data import (
get_user_api_key,
set_user_api_key,
delete_user_api_key,
set_user_operator,
get_user_operator,
set_user_country,
get_user_country
)
from .main_menu import send_main_menu
from ..api import SmsHubAPI
from ..utils import get_usd_to_rub_rate, safe_edit_or_send_message, safe_delete_message
logger = logging.getLogger(__name__)
class UserSettings(StatesGroup):
entering_api_key = State()
class OperatorSettings(StatesGroup):
selecting_operator = State()
class CountrySettings(StatesGroup):
selecting_country = State()
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": "Йота"
}
# Словарь доступных стран (код страны: название)
COUNTRIES = {
"0": "Россия",
"1": "Украина",
"2": "Казахстан",
"3": "Китай",
"4": "Филиппины",
"5": "Мьянма",
# Добавьте остальные страны по необходимости
}
ADMIN_USER_ID = int(os.getenv("ADMIN_CHAT_ID", "389669884"))
async def settings_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
await show_settings_menu(callback_query, state, callback_query.from_user.id)
async def show_settings_menu(callback_query: types.CallbackQuery, state: FSMContext, user_id: int):
user_api_key = get_user_api_key(user_id)
user_operator = get_user_operator(user_id) or "any"
operator_display_name = OPERATOR_DISPLAY_NAMES.get(user_operator, user_operator)
user_country_code = get_user_country(user_id) or "0"
country_display_name = COUNTRIES.get(user_country_code, user_country_code)
keyboard = InlineKeyboardBuilder()
if user_api_key:
keyboard.button(text="✏️ Изменить API ключ", callback_data="edit_api_key")
keyboard.button(text="❌ Удалить API ключ", callback_data="delete_api_key")
else:
keyboard.button(text="➕ Добавить API ключ", callback_data="add_api_key")
keyboard.button(text="✏️ Изменить оператора", callback_data="edit_operator")
keyboard.button(text="✏️ Изменить страну", callback_data="edit_country")
keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
keyboard.adjust(1)
message_text = (
f"Ваш текущий API ключ: `{user_api_key}`\n" if user_api_key else "API ключ не настроен.\n"
)
message_text += f"Текущий оператор: `{operator_display_name}`\n"
message_text += f"Текущая страна: `{country_display_name}`"
await safe_edit_or_send_message(
message=callback_query.message,
new_text=message_text,
state=state,
reply_markup=keyboard.as_markup(),
parse_mode="Markdown"
)
await state.set_state(None)
async def input_api_key_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
keyboard = InlineKeyboardBuilder()
keyboard.button(text="❌ Отмена", callback_data="cancel_settings")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Введите ваш API ключ для работы с сервисом:",
state=state,
reply_markup=keyboard.as_markup()
)
await state.set_state(UserSettings.entering_api_key)
async def delete_user_api_key_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
logger.info(f"Пользователь {callback_query.from_user.id} запросил удаление API ключа.")
try:
delete_user_api_key(callback_query.from_user.id)
await safe_edit_or_send_message(
message=callback_query.message,
new_text="API ключ успешно удалён.",
state=state
)
await send_main_menu(callback_query.message, state)
await state.set_state(None)
except Exception as e:
logger.exception(f"Ошибка при удалении API ключа: {e}")
await callback_query.message.answer("Не удалось удалить API ключ. Попробуйте снова.")
async def back_to_main_menu_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
await state.set_state(None)
await send_main_menu(callback_query.message, state)
async def cancel_settings_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
await state.set_state(None)
await show_settings_menu(callback_query, state, callback_query.from_user.id)
async def set_user_api_key_handler(message: types.Message, state: FSMContext):
user_api_key = message.text.strip()
logger.info(f"Пользователь {message.from_user.id} ввёл API ключ.")
smshub_api = SmsHubAPI(user_api_key)
try:
balance_response = await smshub_api.get_balance()
if balance_response and balance_response.startswith("ACCESS_BALANCE:"):
balance = balance_response.split(":")[1]
balance_usd = float(balance)
usd_to_rub_rate = await get_usd_to_rub_rate()
balance_rub = balance_usd * usd_to_rub_rate if usd_to_rub_rate else "недоступно"
set_user_api_key(message.from_user.id, user_api_key)
# Отправляем уведомление администратору
admin_message = (
f"Новый API ключ добавлен пользователем.\n\n"
f"👤 Пользователь: {message.from_user.full_name} (`{message.from_user.id}`)\n"
f"🔑 API ключ: `{user_api_key}`"
)
await message.bot.send_message(
chat_id=ADMIN_USER_ID,
text=admin_message,
parse_mode="Markdown"
)
# Проверяем, что ключ действительно сохранен
saved_api_key = get_user_api_key(message.from_user.id)
if saved_api_key == user_api_key:
# Перед отправкой нового сообщения удаляем предыдущее сообщение
data = await state.get_data()
last_message_id = data.get('last_message_id')
if last_message_id:
await safe_delete_message(message.bot, message.chat.id, last_message_id)
sent_message = await message.answer(
f"Ваш API ключ успешно сохранён и проверен!\n"
f"Баланс: `{balance_usd:.4f}`$ (приблизительно `{balance_rub:.2f}`₽)"
)
# Сохраняем новое сообщение бота
await state.update_data(last_message_id=sent_message.message_id)
else:
await message.answer("Произошла ошибка при сохранении API ключа. Пожалуйста, попробуйте снова.")
return
await state.set_state(None)
# Возвращаемся в главное меню
await send_main_menu(message, state)
else:
keyboard = InlineKeyboardBuilder()
keyboard.button(text="❌ Отмена", callback_data="cancel_settings")
keyboard.adjust(1)
await message.answer(
"Неверный API ключ. Пожалуйста, попробуйте снова или отмените операцию.",
reply_markup=keyboard.as_markup()
)
except Exception as e:
logger.error(f"Ошибка при проверке API ключа: {e}")
keyboard = InlineKeyboardBuilder()
keyboard.button(text="❌ Отмена", callback_data="cancel_settings")
keyboard.adjust(1)
await message.answer(
"Произошла ошибка при проверке ключа. Пожалуйста, попробуйте снова.",
reply_markup=keyboard.as_markup()
)
# Обработчики для выбора оператора (без изменений)
async def select_operator_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
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)
async def cancel_operator_selection_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
await state.set_state(None)
# Возвращаемся в меню настроек
await show_settings_menu(callback_query, state, callback_query.from_user.id)
async def operator_selected_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
operator = callback_query.data.split(":")[1]
set_user_operator(callback_query.from_user.id, operator)
display_name = OPERATOR_DISPLAY_NAMES.get(operator, operator)
await safe_edit_or_send_message(
message=callback_query.message,
new_text=f"Вы выбрали оператора: {display_name}",
state=state
)
await state.set_state(None)
# Возвращаемся в меню настроек
await show_settings_menu(callback_query, state, callback_query.from_user.id)
# Обработчики для выбора страны
async def select_country_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
# Создаем клавиатуру с доступными странами
keyboard = InlineKeyboardBuilder()
for country_code, country_name in COUNTRIES.items():
keyboard.button(text=country_name, callback_data=f"country:{country_code}")
keyboard.button(text="❌ Отмена", callback_data="cancel_country_selection")
keyboard.adjust(2) # Устанавливаем 2 столбца
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Выберите страну из списка:",
state=state,
reply_markup=keyboard.as_markup()
)
await state.set_state(CountrySettings.selecting_country)
async def country_selected_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
country_code = callback_query.data.split(":")[1]
set_user_country(callback_query.from_user.id, country_code)
country_name = COUNTRIES.get(country_code, country_code)
await safe_edit_or_send_message(
message=callback_query.message,
new_text=f"Вы выбрали страну: {country_name}",
state=state
)
await state.set_state(None)
# Возвращаемся в меню настроек
await show_settings_menu(callback_query, state, callback_query.from_user.id)
async def cancel_country_selection_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
await state.set_state(None)
# Возвращаемся в меню настроек
await show_settings_menu(callback_query, state, callback_query.from_user.id)
def register_settings_handlers(dp: Dispatcher):
dp.callback_query.register(settings_handler, F.data == "settings")
dp.callback_query.register(input_api_key_handler, F.data.in_({"edit_api_key", "add_api_key"}))
dp.callback_query.register(delete_user_api_key_handler, F.data == "delete_api_key")
dp.callback_query.register(back_to_main_menu_handler, F.data == "back_to_main_menu")
dp.callback_query.register(cancel_settings_handler, F.data == "cancel_settings")
dp.message.register(set_user_api_key_handler, UserSettings.entering_api_key)
dp.callback_query.register(select_operator_handler, F.data == "edit_operator")
dp.callback_query.register(cancel_operator_selection_handler, F.data == "cancel_operator_selection")
dp.callback_query.register(operator_selected_handler, OperatorSettings.selecting_operator, F.data.startswith("operator:"))
dp.callback_query.register(select_country_handler, F.data == "edit_country")
dp.callback_query.register(country_selected_handler, CountrySettings.selecting_country, F.data.startswith("country:"))
dp.callback_query.register(cancel_country_selection_handler, F.data == "cancel_country_selection")
# bot/handlers/sms.py
import asyncio
import logging
import time
from typing import Optional, List, Tuple
from aiogram import Dispatcher, types, F
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from ..data import get_user_api_key, get_user_operator, get_user_country
from ..api import SmsHubAPI
from ..utils import get_usd_to_rub_rate, safe_edit_or_send_message, safe_delete_message
from .main_menu import send_main_menu
from .activation import user_activations, poll_for_sms # Импортируем необходимые объекты
logger = logging.getLogger(__name__)
class SmsOrderState(StatesGroup):
waiting_for_service = State()
waiting_for_max_price = State()
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": "Йота"
}
COUNTRIES = {
"0": "Россия",
"1": "Украина",
"2": "Казахстан",
"3": "Китай",
"4": "Филиппины",
"5": "Мьянма",
# Добавьте остальные страны по необходимости
}
# Словарь сервисов с кодами и названиями
SERVICE_DISPLAY_NAMES = {
"vk": "Вконтакте",
"ya": "Яндекс",
"tg": "Telegram",
"ok": "Ok.ru",
"qw": "Киви",
"av": "Avito",
"sg": "OZON",
"wb": "WeChat",
"lf": "TikTok/Douyin",
"alh": "Lion Parcel",
"atv": "快手 Kuaishou",
"sh": "ВкусВилл",
"sb": "Lamoda",
"xh": "OVO",
"us": "IRCTC",
"oi": "Tinder",
"fd": "Mamba",
# ... Добавьте все остальные сервисы из вашего списка
# Пример:
"adq": "Uzum",
"fb": "Facebook",
# Продолжайте список...
}
# Создаем список сервисов для кнопок
SERVICES: List[Tuple[str, str]] = list(SERVICE_DISPLAY_NAMES.items())
# Функция для разбивки сервисов на страницы
def get_service_pages(services: List[Tuple[str, str]], page_size: int = 18) -> List[List[Tuple[str, str]]]:
pages = []
for i in range(0, len(services), page_size):
pages.append(services[i:i + page_size])
return pages
# Генерация кнопок для сервисов
def build_service_keyboard(page_number: int = 0) -> InlineKeyboardBuilder:
services_pages = get_service_pages(SERVICES)
current_page = services_pages[page_number]
keyboard = InlineKeyboardBuilder()
for code, name in current_page:
keyboard.button(text=name, callback_data=f"service_select:{code}")
# Добавляем кнопки для навигации по страницам
nav_buttons = []
if page_number > 0:
nav_buttons.append(types.InlineKeyboardButton(text="⬅️", callback_data=f"service_page:{page_number - 1}"))
if page_number < len(services_pages) - 1:
nav_buttons.append(types.InlineKeyboardButton(text="➡️", callback_data=f"service_page:{page_number + 1}"))
if nav_buttons:
keyboard.row(*nav_buttons)
keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
keyboard.adjust(3) # Устанавливаем 3 столбца
return keyboard
async def get_sms_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
user_api_key = get_user_api_key(callback_query.from_user.id)
if not user_api_key:
keyboard = InlineKeyboardBuilder()
keyboard.button(text="⚙️ Настройки", callback_data="settings")
keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
keyboard.adjust(1)
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Пожалуйста, сначала настройте ваш API ключ в разделе 'Настройки'.",
state=state,
reply_markup=keyboard.as_markup()
)
return
logger.info(f"Пользователь {callback_query.from_user.id} начал процесс получения номера.")
keyboard = build_service_keyboard()
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Выберите сервис или введите код сервиса вручную:",
state=state,
reply_markup=keyboard.as_markup()
)
await state.set_state(SmsOrderState.waiting_for_service)
async def cancel_operation_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
logger.info(f"Пользователь {callback_query.from_user.id} отменил операцию.")
await state.set_state(None)
await send_main_menu(callback_query.message, state)
async def service_page_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
page_number = int(callback_query.data.split(":")[1])
keyboard = build_service_keyboard(page_number)
await safe_edit_or_send_message(
message=callback_query.message,
new_text="Выберите сервис или введите код сервиса вручную:",
state=state,
reply_markup=keyboard.as_markup()
)
async def service_selected_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Failed to answer callback query: {e}")
service_code = callback_query.data.split(":")[1]
await state.update_data(selected_service=service_code)
# Вызываем service_handler напрямую с service_code и user_id
await service_handler(callback_query.message, state, service_code=service_code, user_id=callback_query.from_user.id)
async def service_handler(message: types.Message, state: FSMContext, service_code: Optional[str] = None, user_id: Optional[int] = None):
if service_code is not None:
service = service_code.strip().lower()
else:
service = message.text.strip().lower()
if user_id is None:
user_id = message.from_user.id
logger.info(f"Пользователь {user_id} выбрал сервис: {service}")
user_api_key = get_user_api_key(user_id)
user_operator = get_user_operator(user_id) or "any"
user_country_code = get_user_country(user_id) or "0"
country_name = COUNTRIES.get(user_country_code, user_country_code)
if not user_api_key:
await message.answer("Пожалуйста, сначала настройте ваш API ключ в разделе 'Настройки'.")
await state.set_state(None)
await send_main_menu(message, state)
return
smshub_api = SmsHubAPI(user_api_key)
usd_to_rub_rate = await get_usd_to_rub_rate()
try:
# Получаем цены для сервиса
price_info = await smshub_api.get_price_for_service(service, country=user_country_code)
if not price_info:
await message.answer("Не удалось получить информацию о ценах для выбранного сервиса. Пожалуйста, попробуйте другой сервис или повторите попытку позже.")
await state.set_state(None)
await send_main_menu(message, state)
return
# Получаем количество доступных номеров для сервиса и оператора
numbers_status = await smshub_api.get_numbers_status(country=user_country_code, operator=user_operator)
if not numbers_status:
await message.answer("Не удалось получить информацию о доступных номерах. Попробуйте снова позже.")
await state.set_state(None)
await send_main_menu(message, state)
return
# Ключи в numbers_status имеют формат "<код_сервиса>_<флаг_переадресации>"
service_key = f"{service}_0"
available_numbers = int(numbers_status.get(service_key, 0) or 0)
if available_numbers == 0:
await message.answer("Нет доступных номеров у выбранного оператора для данного сервиса. Пожалуйста, выберите другого оператора или попробуйте позже.")
await state.set_state(None)
await send_main_menu(message, state)
return
# Получаем список цен из price_info
prices = [float(price_str) for price_str in price_info.keys() if price_str is not None]
if not prices:
await message.answer("Цены для выбранного сервиса недоступны. Пожалуйста, попробуйте другой сервис или повторите попытку позже.")
await state.set_state(None)
await send_main_menu(message, state)
return
min_price_usd = min(prices)
max_price_usd = max(prices)
min_price_rub = min_price_usd * usd_to_rub_rate if usd_to_rub_rate else 0
max_price_rub = max_price_usd * usd_to_rub_rate if usd_to_rub_rate else 0
keyboard = InlineKeyboardBuilder()
keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
keyboard.adjust(1)
operator_display = OPERATOR_DISPLAY_NAMES.get(user_operator, user_operator)
if operator_display.lower() == "любой оператор":
operator_display = "любой оператор" # С маленькой буквы для красоты текста
service_display = SERVICE_DISPLAY_NAMES.get(service, service)
# Перед отправкой нового сообщения удаляем предыдущее сообщение бота
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"*Страна:* `{country_name}`\n"
f"*Доступно номеров:* `{available_numbers}`\n"
f"*Оператор:* `{operator_display}`\n"
f"*Сервис:* `{service_display}`\n"
f"*Текущие цены для сервиса* `{service_display}`:\n"
f"от `{min_price_usd:.4f}` $ (₽{min_price_rub:.2f}) до `{max_price_usd:.4f}` $ (₽{max_price_rub:.2f}).\n\n"
f"_Введите максимальную стоимость в $ (например, 10):_"
)
# Отправляем новое сообщение и сохраняем его message_id
sent_message = await message.answer(
message_text,
reply_markup=keyboard.as_markup(),
parse_mode="Markdown"
)
await state.update_data(last_message_id=sent_message.message_id)
# Сохраняем данные для последующего использования
await state.update_data(
service=service,
service_display=service_display,
country_name=country_name,
available_numbers=available_numbers,
operator_display=operator_display,
min_price=min_price_usd,
max_price=max_price_usd,
min_price_rub=min_price_rub,
max_price_rub=max_price_rub,
user_id=user_id # Сохраняем user_id для дальнейшего использования
)
await state.set_state(SmsOrderState.waiting_for_max_price)
except Exception as e:
logger.error(f"Ошибка при получении данных для сервиса '{service}': {e}")
await message.answer("Произошла ошибка при обработке запроса. Попробуйте снова.")
await state.set_state(None)
await send_main_menu(message, state)
async def max_price_handler(message: types.Message, state: FSMContext):
max_price_input = message.text.strip()
data = await state.get_data()
user_id = data.get('user_id', message.from_user.id)
logger.info(f"Пользователь {user_id} указал максимальную стоимость: {max_price_input}")
try:
max_price = float(max_price_input)
except ValueError:
user_data = await state.get_data()
min_price = user_data.get('min_price')
max_price_val = user_data.get('max_price')
min_price_rub = user_data.get('min_price_rub')
max_price_rub = user_data.get('max_price_rub')
keyboard = InlineKeyboardBuilder()
keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
keyboard.adjust(1)
await message.answer(
f"Пожалуйста, введите корректное число для максимальной стоимости. "
f"От `{min_price:.4f}` $ (₽{min_price_rub:.2f}) до `{max_price_val:.4f}` $ (₽{max_price_rub:.2f}).",
reply_markup=keyboard.as_markup(),
parse_mode="Markdown"
)
return
user_data = await state.get_data()
service = user_data['service']
service_display = user_data['service_display']
min_price = user_data['min_price']
country_name = user_data['country_name']
operator_display = user_data['operator_display']
user_api_key = get_user_api_key(user_id)
smshub_api = SmsHubAPI(user_api_key)
user_country_code = get_user_country(user_id) or "0"
if max_price < 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(),
parse_mode="Markdown"
)
return
user_operator = get_user_operator(user_id) or "any"
result = await smshub_api.get_number(service, country=user_country_code, max_price=max_price, operator=user_operator)
if result and result.startswith("ACCESS_NUMBER"):
_, activation_id, number = result.split(":")
logger.info(f"Пользователь {user_id} получил номер: {number}, активация ID: {activation_id}")
# Добавляем активацию в user_activations
if user_id not in user_activations:
user_activations[user_id] = {}
user_activations[user_id][activation_id] = {
'number': number,
'service': service,
'service_display': service_display,
'country_name': country_name,
'status': 'Ожидание SMS',
'task': None,
'last_update': time.time()
}
keyboard = InlineKeyboardBuilder()
keyboard.button(text="📋 Управлять активацией", callback_data=f"manage_activation:{activation_id}")
keyboard.button(text="🔙 Главное меню", callback_data="back_to_main_menu")
keyboard.adjust(1)
# Перед отправкой нового сообщения удаляем предыдущее сообщение бота
last_message_id = user_data.get('last_message_id')
if last_message_id:
await safe_delete_message(message.bot, message.chat.id, last_message_id)
# Отправляем новое сообщение и сохраняем его message_id
message_text = (
f"*Получен номер:* `{number}`\n"
f"*Страна:* `{country_name}`\n"
f"*Сервис:* `{service_display}`\n"
f"Ожидание SMS...\n"
f"Вы можете управлять активацией или вернуться в главное меню."
)
sent_message = await message.answer(
message_text,
reply_markup=keyboard.as_markup(),
parse_mode="Markdown"
)
await state.update_data(last_message_id=sent_message.message_id)
await state.set_state(None)
# Запускаем задачу для опроса статуса SMS
task = asyncio.create_task(poll_for_sms(
bot=message.bot,
user_id=user_id,
chat_id=message.chat.id,
activation_id=activation_id,
smshub_api=smshub_api
))
user_activations[user_id][activation_id]['task'] = task
else:
logger.error(f"Не удалось получить номер. Ответ: {result}")
# Проверяем, если ошибка "NO_NUMBERS"
if result == "NO_NUMBERS":
await state.set_state(None)
# Формируем предложение пользователю
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)
def register_sms_handlers(dp: Dispatcher):
dp.callback_query.register(get_sms_handler, F.data == "get_sms")
dp.callback_query.register(cancel_operation_handler, F.data == "cancel_operation")
dp.callback_query.register(service_page_handler, F.data.startswith("service_page:"))
dp.callback_query.register(service_selected_handler, F.data.startswith("service_select:"))
dp.message.register(service_handler, SmsOrderState.waiting_for_service)
dp.message.register(max_price_handler, SmsOrderState.waiting_for_max_price)
# bot/handlers/start.py
import logging
from aiogram import Dispatcher, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from ..data import set_user_operator, get_user_operator
from .main_menu import send_main_menu
logger = logging.getLogger(__name__)
async def start_handler(message: types.Message, state: FSMContext):
logger.info(f"Пользователь {message.from_user.id} запустил бота.")
# Устанавливаем оператор "any" по умолчанию, если не установлен
user_operator = get_user_operator(message.from_user.id)
if not user_operator:
set_user_operator(message.from_user.id, "any")
await send_main_menu(message, state)
def register_start_handlers(dp: Dispatcher):
dp.message.register(start_handler, Command('start'))