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

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 ..constants import SERVICE_DISPLAY_NAMES

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"Не удалось ответить на 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: types.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:
    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text="Активация не найдена.",
        state=state,
        reply_markup=callback_query.message.reply_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}")
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"*Сервис:* {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: types.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}")

    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"Не удалось ответить на 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"Не удалось ответить на 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()

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

            # Удаляем предыдущее сообщение, чтобы с новыми появилось уведомление
            previous_message_id = activation.get("message_id")
            await safe_delete_message(bot, chat_id, previous_message_id)

            # Отправляем новое сообщение
            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

            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"))

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)
    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:
try:
await bot.delete_message(chat_id=chat_id, message_id=message_id)
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")

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