Nov 13 20:01:35 vps1707806210 python[32330]: 2024-11-13 20:01:35,966 - bot.api - INFO - Ответ статуса: STATUS_WAIT_CODE
Nov 13 20:01:38 vps1707806210 python[32330]: 2024-11-13 20:01:38,970 - bot.api - INFO - Запрос статуса для активации: 733411393
Nov 13 20:01:39 vps1707806210 python[32330]: 2024-11-13 20:01:39,717 - bot.api - INFO - Ответ статуса: STATUS_OK:6197
Nov 13 20:01:39 vps1707806210 python[32330]: 2024-11-13 20:01:39,888 - bot.utils - INFO - Сообщение с ID 267 успешно удалено.
Nov 13 20:01:39 vps1707806210 python[32330]: 2024-11-13 20:01:39,889 - bot.handlers.activation - INFO - Сообщение с ID 267 удалено.
Nov 13 20:01:40 vps1707806210 python[32330]: 2024-11-13 20:01:40,062 - bot.handlers.activation - INFO - Новое сообщение с кодом активации отправлено с ID 268
Nov 13 20:01:44 vps1707806210 python[32330]: 2024-11-13 20:01:44,053 - bot.utils - ERROR - Ошибка при редактировании сообщения: Telegram server says - Bad Request: message to edit not found
Nov 13 20:01:44 vps1707806210 python[32330]: 2024-11-13 20:01:44,116 - bot.utils - ERROR - Не удалось удалить сообщение: Telegram server says - Bad Request: message to delete not found
Nov 13 20:01:44 vps1707806210 python[32330]: 2024-11-13 20:01:44,286 - aiogram.event - INFO - Update id=600811684 is handled. Duration 428 ms by bot id=7633673561


ничего не изменилось.
сообщения с кодом активации должны удалять любое предыдущее сообщение от бота и заменяться новым сообщением с кодом, тогда как все остальные сообщения должны редактировать свой текст с помощью функции safe_edit_or_send_message. и после того, как код активации получен, так же при помощи этой функции должны сообщения изменять текст сообщеняи с кодом активации, не нужно создавтаь новое:

1. # /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 aiogram.types import CallbackQuery

from ..data import get_user_api_key
from ..api import SmsHubAPI
from ..utils import safe_edit_or_send_message, safe_delete_message
from ..constants.services import SERVICE_DISPLAY_NAMES

logger = logging.getLogger(__name__)

# Глобальная переменная для активаций
# Структура: {user_id: {activation_id: {data: ..., message_id: ...}}}
user_activations: Dict[int, Dict[str, Dict[str, Any]]] = {}


async def current_activations_handler(callback_query: CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")
        
    user_id = callback_query.from_user.id
    activations = user_activations.get(user_id, {})

    if not activations:
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
        keyboard.adjust(1)
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="У вас нет активных активаций.",
            state=state,
            reply_markup=keyboard.as_markup()
        )
        return

    keyboard = InlineKeyboardBuilder()
    for activation_id, data in activations.items():
        service_name = SERVICE_DISPLAY_NAMES.get(data['service'], data['service'])
        keyboard.button(
            text=f"{service_name} ({data['number']}) - {data['status']}",
            callback_data=f"manage_activation:{activation_id}"
        )
    keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
    keyboard.adjust(1)

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text="Ваши текущие активации:",
        state=state,
        reply_markup=keyboard.as_markup()
    )


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

    user_id = callback_query.from_user.id
    activation_id = callback_query.data.split(":")[1]
    activations = user_activations.get(user_id, {})
    activation = activations.get(activation_id)

    if not activation:
        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()
        )
        return

    service_name = SERVICE_DISPLAY_NAMES.get(activation['service'], activation['service'])

    keyboard = InlineKeyboardBuilder()

    if activation['status'] == 'Ожидание SMS':
        keyboard.button(text="❌ Отменить активацию", callback_data=f"cancel_activation:{activation_id}")
    elif activation['status'].startswith('Код получен'):
        keyboard.button(text="📂 Текущие активации", callback_data="current_activations")
        keyboard.button(text="✅ Завершить активацию", callback_data=f"complete_activation:{activation_id}")
    elif activation['status'].startswith('Ожидание повторного SMS'):
        keyboard.button(text="✅ Завершить активацию", callback_data=f"complete_activation:{activation_id}")
        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)

    # Редактируем сообщение напрямую через message_id активации
    message_id = activation.get('message_id')
    if message_id:
        try:
            await callback_query.bot.edit_message_text(
                chat_id=user_id,
                message_id=message_id,
                text=(
                    f"*Активация:* {activation_id}\n"
                    f"*Сервис:* {service_name}\n"
                    f"*Номер:* `{activation['number']}`\n"
                    f"*Статус:* {activation['status']}"
                ),
                reply_markup=keyboard.as_markup(),
                parse_mode="Markdown"
            )
        except TelegramBadRequest as e:
            logger.error(f"Не удалось отредактировать сообщение с ID {message_id}: {e}")
            # В случае ошибки, можно отправить новое сообщение
            sent_message = await callback_query.message.answer(
                text=(
                    f"*Активация:* {activation_id}\n"
                    f"*Сервис:* {service_name}\n"
                    f"*Номер:* `{activation['number']}`\n"
                    f"*Статус:* {activation['status']}"
                ),
                reply_markup=keyboard.as_markup(),
                parse_mode="Markdown"
            )
            activation['message_id'] = sent_message.message_id
    else:
        # Если нет сохранённого message_id, используем safe_edit_or_send_message
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text=(
                f"*Активация:* {activation_id}\n"
                f"*Сервис:* {service_name}\n"
                f"*Номер:* `{activation['number']}`\n"
                f"*Статус:* {activation['status']}"
            ),
            state=state,
            reply_markup=keyboard.as_markup(),
            parse_mode="Markdown"
        )


async def cancel_activation_handler(callback_query: CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")
    
    user_id = callback_query.from_user.id
    activation_id = callback_query.data.split(":")[1]
    user_api_key = get_user_api_key(user_id)
    smshub_api = SmsHubAPI(user_api_key)

    activations = user_activations.get(user_id, {})
    activation = activations.get(activation_id)

    if activation:
        task = activation.get('task')
        if task:
            task.cancel()

        result = await smshub_api.cancel_activation(activation_id)
        logger.info(f"Активация {activation_id} отменена на стороне API: {result}")

        # Удаляем сообщение с кодом активации, если оно существует
        message_id = activation.get('message_id')
        if message_id:
            await safe_delete_message(callback_query.bot, user_id, message_id)

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


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

    user_id = callback_query.from_user.id
    activation_id = callback_query.data.split(":")[1]
    user_api_key = get_user_api_key(user_id)
    smshub_api = SmsHubAPI(user_api_key)
    activations = user_activations.get(user_id, {})
    activation = activations.get(activation_id)

    if not activation:
        await callback_query.message.answer("Активация не найдена.")
        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.bot,
            user_id=user_id,
            chat_id=callback_query.message.chat.id,
            activation_id=activation_id,
            smshub_api=smshub_api
        ))
        activation['task'] = task
        activation['status'] = 'Ожидание повторного SMS'

        # Обновляем сообщение о статусе активации
        await manage_activation_handler(callback_query, state)
    else:
        logger.error(f"Не удалось запросить повторное SMS. Ответ: {result}")
        await callback_query.message.answer(
            "Не удалось запросить повторный SMS. Попробуйте снова."
        )


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

    user_id = callback_query.from_user.id
    activation_id = callback_query.data.split(":")[1]
    user_api_key = get_user_api_key(user_id)
    smshub_api = SmsHubAPI(user_api_key)
    activations = user_activations.get(user_id, {})
    activation = activations.get(activation_id)

    if not activation:
        await callback_query.message.answer("Активация не найдена.")
        return

    result = await smshub_api.set_status(activation_id, status="6")
    if result == "ACCESS_ACTIVATION":
        task = activation.get('task')
        if task:
            task.cancel()

        # Удаляем сообщение с кодом активации, если оно существует
        message_id = activation.get('message_id')
        if message_id:
            try:
                # Удаляем сообщение с кодом активации
                await safe_delete_message(callback_query.bot, user_id, message_id)
                logger.info(f"Сообщение с ID {message_id} удалено.")
            except Exception as e:
                logger.error(f"Ошибка при удалении сообщения: {e}")
                # В случае ошибки можно отправить новое сообщение
                await callback_query.message.answer(
                    "Активация успешно завершена."
                )

        del activations[activation_id]

        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="🔙 Назад", callback_data="current_activations")
        keyboard.adjust(1)

        # Обновляем список активаций
        if activations:
            await current_activations_handler(callback_query, state)
        else:
            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 callback_query.message.answer(
            "Не удалось завершить активацию. Попробуйте снова."
        )


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

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

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

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

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

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

            if status in ["STATUS_WAIT_CODE", "STATUS_WAIT_RETRY"]:
                activation['last_update'] = time.time()
                continue

            elif status == "STATUS_CANCEL":
                await bot.send_message(chat_id, "Активация была отменена.")
                activation['status'] = 'Активация отменена'

                # Удаляем предыдущее сообщение с кодом активации, если существует
                message_id = activation.get('message_id')
                if message_id:
                    await safe_delete_message(bot, user_id, message_id)

                del activations[activation_id]
                return

            elif status == "STATUS_OK":
                code = status_parts[1] if len(status_parts) > 1 else "Код не получен"
                activation['status'] = f'Код получен: `{code}`'
                activation['last_update'] = time.time()

                # Создаем инлайн-клавиатуру
                keyboard = InlineKeyboardBuilder()
                keyboard.button(text="📂 Текущие активации", callback_data="current_activations")
                keyboard.button(text="✅ Завершить активацию", callback_data=f"complete_activation:{activation_id}")
                keyboard.adjust(1)

                # Удаляем предыдущее сообщение с кодом активации, если оно существует
                message_id = activation.get('message_id')
                if message_id:
                    try:
                        await safe_delete_message(bot, user_id, message_id)
                        logger.info(f"Сообщение с ID {message_id} удалено.")
                    except Exception as e:
                        logger.error(f"Ошибка при удалении сообщения: {e}")

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

                return

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


async def back_to_main_menu_handler(callback_query: CallbackQuery, state: FSMContext):
    """
    Обработчик для кнопки "🔙 Назад", которая возвращает пользователя в главное меню.
    Использует функцию safe_edit_or_send_message для редактирования текущего сообщения.
    """
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")

    main_menu_text = "Главное меню:\nВыберите действие ниже."
    main_menu_keyboard = InlineKeyboardBuilder()
    main_menu_keyboard.button(text="📂 Текущие активации", callback_data="current_activations")
    main_menu_keyboard.adjust(1)

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text=main_menu_text,
        state=state,
        reply_markup=main_menu_keyboard.as_markup()
    )


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"))
    dp.callback_query.register(back_to_main_menu_handler, F.data == "back_to_main_menu")


2. # /bot/utils.py

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

logger = logging.getLogger(__name__)

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

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


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


async def safe_edit_or_send_message(
    message: types.Message,
    new_text: str,
    state: FSMContext,
    **kwargs
) -> Optional[types.Message]:
    """
    Эта функция используется для редактирования последнего сообщения пользователя.
    Если редактирование невозможно, сообщение удаляется и отправляется новое.
    Используется для всех сообщений, кроме сообщений с кодом активации.
    """
    data = await state.get_data()
    last_message_id = data.get('last_message_id')
    last_message_text = data.get('last_message_text')
    last_reply_markup = data.get('last_reply_markup')

    new_reply_markup = kwargs.get("reply_markup")

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

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

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


async def safe_delete_message(bot: Bot, chat_id: int, message_id: int) -> None:
    """
    Безопасно удаляет сообщение по chat_id и message_id.
    """
    try:
        await bot.delete_message(chat_id=chat_id, message_id=message_id)
        logger.info(f"Сообщение с ID {message_id} успешно удалено.")
    except TelegramBadRequest as e:
        if "message to delete not found" in str(e).lower():
            logger.warning(f"Сообщение с ID {message_id} не найдено или уже удалено.")
        else:
            logger.error(f"Не удалось удалить сообщение: {e}")
    except Exception as e:
        logger.error(f"Не удалось удалить сообщение: {e}")


async def on_startup(dispatcher: Dispatcher):
    logger.info("Бот успешно запущен.")