User: Анализ файла bot.py:

Содержимое:
import logging
import asyncio
import os
import sqlite3
from datetime import datetime, timedelta
from typing import Dict, List

from aiogram import Bot, Dispatcher, types
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram.dispatcher import FSMContext
from aiogram.dispatcher.filters.state import State, StatesGroup

# Импортируем настройки из config.py
import config

# Импортируем клиенты API
from sparrow_api import SparrowAPI
from gpt4o_api import GPT4OAPI
from forgetapi import ForgetAPI
from image_api import ImageAPI

# Импортируем AudioProcessor из audio_processing.py
from audio_processing import AudioProcessor

# Импортируем функции из doc.py
from doc import process_file_content, send_long_message, build_context, SUPPORTED_EXTENSIONS

# Дополнительные импорты
import xlsxwriter

# Настройка логирования
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


# Классы состояний для FSM
class Form(StatesGroup):
    ImageGenerationPrompt = State()
    ImageGenerationSize = State()
    # Состояния для Админ-Панели
    AdminPanel = State()
    SelectTariffForEdit = State()
    EditTariffNameOrDescription = State()
    EditTariffTokens = State()
    BroadcastMessageState = State()
    BroadcastSelectTariff = State()
    EditTariffName = State()
    EditTariffDescription = State()
    EditTariffTokenAmount = State()
    # Новые состояния для управления чатами
    ManageChats = State()
    AddChat = State()
    DeleteChat = State()
    # Состояния для генерации отчёта
    GenerateReport = State()


class EnhancedBot:
    MAX_MESSAGE_LENGTH = 4000  # Максимальная длина сообщения

    # Доступные модели и их API
    ALL_AVAILABLE_MODELS = [
        ("GPT-4o", "gpt-4o-2024-08-06", "gpt4o"),
        ("o1-preview", "o1-preview", "forget"),
        ("o1-mini", "o1-mini", "forget"),
        ("GPT 4o Mini", "gpt-4o-mini", "forget"),
        ("GPT 3.5 turbo", "gpt-3.5-turbo", "forget"),
        # Добавьте другие модели по необходимости
    ]

    FREE_MODELS = ALL_AVAILABLE_MODELS  # Теперь все модели доступны бесплатно

    def __init__(self):
        self.bot = Bot(token=config.BOT_TOKEN)
        self.storage = MemoryStorage()
        self.dp = Dispatcher(self.bot, storage=self.storage)
        self.sparrow = SparrowAPI()
        self.gpt4o_api = GPT4OAPI()
        self.forget_api = ForgetAPI()
        self.image_api = ImageAPI()
        self.audio_processor = AudioProcessor(self.bot)
        self.conversations: Dict[str, List[str]] = {}
        self.waiting_messages: Dict[str, types.Message] = {}
        self.user_tasks: Dict[str, asyncio.Task] = {}
        self.db_connection = sqlite3.connect('user_data.db')
        self.db_cursor = self.db_connection.cursor()
        self._setup_database()
        self._setup_handlers()

        # Инициализация API клиентов
        self.api_clients = {
            "sparrow": self.sparrow,
            "gpt4o": self.gpt4o_api,
            "forget": self.forget_api,
            "image": self.image_api,
        }

        # Создание маппинга модель -> API
        self.MODEL_API_MAPPING = {
            model_id: api_name for (_, model_id, api_name) in self.ALL_AVAILABLE_MODELS
        }

    def _setup_database(self):
        # Создание таблицы для хранения данных пользователей
        self.db_cursor.execute('''
            CREATE TABLE IF NOT EXISTS users (
                user_id TEXT PRIMARY KEY,
                is_subscribed INTEGER DEFAULT 0,
                model TEXT DEFAULT 'gpt-4-turbo',
                tariff_plan TEXT DEFAULT 'free',
                tokens_available INTEGER DEFAULT 3000,
                last_token_refresh DATE,
                group_association TEXT
            )
        ''')
        # Создание таблицы для тарифных планов
        self.db_cursor.execute('''
            CREATE TABLE IF NOT EXISTS tariff_plans (
                tariff_id TEXT PRIMARY KEY,
                name TEXT,
                description TEXT,
                daily_tokens INTEGER
            )
        ''')
        # Создание таблицы для премиум-чатов
        self.db_cursor.execute('''
            CREATE TABLE IF NOT EXISTS premium_chats (
                chat_id INTEGER PRIMARY KEY
            )
        ''')
        # Инициализируем тарифные планы, если таблица пуста
        self.db_cursor.execute('SELECT COUNT(*) FROM tariff_plans')
        if self.db_cursor.fetchone()[0] == 0:
            self.db_cursor.executemany('''
                INSERT INTO tariff_plans (tariff_id, name, description, daily_tokens) VALUES (?, ?, ?, ?)
            ''', [
                ('free', 'FREE', 'Стандартный тариф с ограниченным набором функций.', 3000),
                ('premium', 'Premium', 'Расширенный тариф с дополнительными возможностями.', 100000),
                ('admin', 'ADMIN', 'Неограниченный доступ для администраторов.', -1)
            ])
            self.db_connection.commit()
        # Инициализируем премиум-чаты, если таблица пуста
        self.db_cursor.execute('SELECT COUNT(*) FROM premium_chats')
        if self.db_cursor.fetchone()[0] == 0:
            self.db_cursor.executemany('''
                INSERT INTO premium_chats (chat_id) VALUES (?)
            ''', [
                (-1002413809826,),
                (-1002482241267,)
            ])
            self.db_connection.commit()
        self.db_connection.commit()

    def get_tariff_info(self, tariff_id: str):
        self.db_cursor.execute('SELECT name, description, daily_tokens FROM tariff_plans WHERE tariff_id = ?', (tariff_id,))
        result = self.db_cursor.fetchone()
        if result:
            return {'name': result[0], 'description': result[1], 'daily_tokens': result[2]}
        else:
            return None

    def set_tariff_info(self, tariff_id: str, name: str = None, description: str = None, daily_tokens: int = None):
        if name:
            self.db_cursor.execute('UPDATE tariff_plans SET name = ? WHERE tariff_id = ?', (name, tariff_id))
        if description:
            self.db_cursor.execute('UPDATE tariff_plans SET description = ? WHERE tariff_id = ?', (description, tariff_id))
        if daily_tokens is not None:
            self.db_cursor.execute('UPDATE tariff_plans SET daily_tokens = ? WHERE tariff_id = ?', (daily_tokens, tariff_id))
        self.db_connection.commit()

    def is_user_subscribed(self, user_id: str) -> bool:
        # Проверка подписки пользователя
        self.db_cursor.execute('SELECT is_subscribed FROM users WHERE user_id = ?', (user_id,))
        result = self.db_cursor.fetchone()
        if result:
            return bool(result[0])
        else:
            return False

    def set_user_subscription(self, user_id: str, is_subscribed: bool):
        # Установка статуса подписки пользователя
        self.db_cursor.execute('''
            INSERT INTO users (user_id, is_subscribed) VALUES (?, ?)
            ON CONFLICT(user_id) DO UPDATE SET is_subscribed=?
        ''', (user_id, int(is_subscribed), int(is_subscribed)))
        self.db_connection.commit()

    def get_user_model(self, user_id: str) -> str:
        # Получение выбранной модели пользователя
        self.db_cursor.execute('SELECT model FROM users WHERE user_id = ?', (user_id,))
        result = self.db_cursor.fetchone()
        if result:
            return result[0]
        else:
            # Устанавливаем модель по умолчанию в соответствии с тарифом
            return 'gpt-4-turbo'  # Модель по умолчанию

    def set_user_model(self, user_id: str, model: str):
        # Установка выбранной модели пользователя
        self.db_cursor.execute('''
            INSERT INTO users (user_id, model) VALUES (?, ?)
            ON CONFLICT(user_id) DO UPDATE SET model=?
        ''', (user_id, model, model))
        self.db_connection.commit()

    def get_user_tariff_plan(self, user_id: str) -> str:
        # Получение тарифного плана пользователя
        self.db_cursor.execute('SELECT tariff_plan FROM users WHERE user_id = ?', (user_id,))
        result = self.db_cursor.fetchone()
        if result:
            return result[0]
        else:
            return 'free'  # Тариф по умолчанию

    def set_user_tariff_plan(self, user_id: str, tariff_plan: str):
        # Установка тарифного плана пользователя
        # Также выполняем перерасчет токенов при смене тарифного плана
        previous_tariff_plan = self.get_user_tariff_plan(user_id)
        self.db_cursor.execute('''
            INSERT INTO users (user_id, tariff_plan) VALUES (?, ?)
            ON CONFLICT(user_id) DO UPDATE SET tariff_plan=?
        ''', (user_id, tariff_plan, tariff_plan))
        self.db_connection.commit()
        if previous_tariff_plan != tariff_plan:
            # Перерасчет токенов
            self.set_last_token_refresh(user_id, None)
            asyncio.create_task(self.refresh_user_tokens(user_id))

    def get_user_tokens(self, user_id: str) -> int:
        # Получение количества токенов пользователя
        self.db_cursor.execute('SELECT tokens_available FROM users WHERE user_id = ?', (user_id,))
        result = self.db_cursor.fetchone()
        if result:
            return result[0]
        else:
            # Устанавливаем токены по умолчанию в соответствии с тарифом
            tariff_plan = self.get_user_tariff_plan(user_id)
            tariff_info = self.get_tariff_info(tariff_plan)
            return tariff_info['daily_tokens'] if tariff_info else 3000

    def set_user_tokens(self, user_id: str, tokens: int):
        # Установка количества токенов пользователя
        self.db_cursor.execute('''
            INSERT INTO users (user_id, tokens_available) VALUES (?, ?)
            ON CONFLICT(user_id) DO UPDATE SET tokens_available=?
        ''', (user_id, tokens, tokens))
        self.db_connection.commit()

    def get_last_token_refresh(self, user_id: str) -> datetime:
        # Получение даты последнего обновления токенов
        self.db_cursor.execute('SELECT last_token_refresh FROM users WHERE user_id = ?', (user_id,))
        result = self.db_cursor.fetchone()
        if result and result[0]:
            return datetime.strptime(result[0], '%Y-%m-%d %H:%M:%S.%f')
        else:
            return None

    def set_last_token_refresh(self, user_id: str, date: datetime):
        # Установка даты последнего обновления токенов
        date_str = date.strftime('%Y-%m-%d %H:%M:%S.%f') if date else None
        self.db_cursor.execute('''
            INSERT INTO users (user_id, last_token_refresh) VALUES (?, ?)
            ON CONFLICT(user_id) DO UPDATE SET last_token_refresh=?
        ''', (user_id, date_str, date_str))
        self.db_connection.commit()

    async def refresh_user_tokens(self, user_id: str):
        # Обновление токенов пользователя ежедневно
        last_refresh = self.get_last_token_refresh(user_id)
        now = datetime.now()
        if not last_refresh or (now - last_refresh).days >= 1:
            tariff_plan = self.get_user_tariff_plan(user_id)
            tariff_info = self.get_tariff_info(tariff_plan)
            tokens = tariff_info['daily_tokens'] if tariff_info else 3000
            self.set_user_tokens(user_id, tokens)
            self.set_last_token_refresh(user_id, now)

    async def check_and_update_tariff_plan(self, user_id: str):
        # Проверка и обновление тарифного плана пользователя
        telegram_user_id = int(user_id)
        is_admin = telegram_user_id in config.ADMIN_IDS

        if is_admin:
            self.set_user_tariff_plan(user_id, 'admin')
        else:
            # Получаем список премиум-чатов из базы данных
            self.db_cursor.execute('SELECT chat_id FROM premium_chats')
            premium_chat_ids = [row[0] for row in self.db_cursor.fetchall()]

            # Проверяем, состоит ли пользователь в одной из премиум-групп
            is_premium_member = False
            for group_id in premium_chat_ids:
                try:
                    chat_member = await self.bot.get_chat_member(chat_id=group_id, user_id=telegram_user_id)
                    if chat_member.status in ['member', 'administrator', 'creator']:
                        is_premium_member = True
                        # Обновляем информацию о принадлежности пользователя к группе
                        self.set_user_group_association(user_id, group_id)
                        break
                except Exception as e:
                    logger.error(f"Ошибка при проверке членства в группе {group_id} для пользователя {user_id}: {e}")

            if is_premium_member:
                self.set_user_tariff_plan(user_id, 'premium')
            else:
                self.set_user_tariff_plan(user_id, 'free')
                # Устанавливаем модель по умолчанию для бесплатного тарифа
                self.set_user_model(user_id, 'gpt-4-turbo')

    def set_user_group_association(self, user_id: str, group_id: int):
        # Устанавливаем информацию о принадлежности пользователя к группе
        self.db_cursor.execute('''
            INSERT INTO users (user_id, group_association) VALUES (?, ?)
            ON CONFLICT(user_id) DO UPDATE SET group_association=?
        ''', (user_id, str(group_id), str(group_id)))
        self.db_connection.commit()

    def get_user_group_association(self, user_id: str) -> str:
        # Получаем информацию о принадлежности пользователя к группе
        self.db_cursor.execute('SELECT group_association FROM users WHERE user_id = ?', (user_id,))
        result = self.db_cursor.fetchone()
        if result and result[0]:
            return result[0]
        else:
            return "None"

    def _setup_handlers(self):
        # Настройка обработчиков сообщений
        self.dp.register_message_handler(self.cmd_start, commands=['start', 'help'])
        self.dp.register_message_handler(
            self.process_model_selection,
            lambda msg: msg.text in [model[0] for model in self.ALL_AVAILABLE_MODELS]
        )
        self.dp.register_message_handler(
            self.handle_clear_context,
            lambda msg: msg.text == "🗑 Очистить контекст"
        )
        self.dp.register_message_handler(
            self.show_model_selection,
            lambda msg: msg.text == "🤖 Выбрать модель"
        )
        self.dp.register_message_handler(
            self.show_profile,
            lambda msg: msg.text == "👤 Профиль"
        )
        self.dp.register_message_handler(
            self.start_image_generation,
            lambda msg: msg.text == "🖼️ Генерация изображения"
        )
        self.dp.register_message_handler(
            self.check_subscription_button_handler,
            lambda msg: msg.text == "🔄 Проверить подписку"
        )
        self.dp.register_message_handler(
            self.cancel_handler,
            state='*',
            text='🔙 Назад'
        )
        # Обработчики для Админ-Панели
        self.dp.register_message_handler(
            self.show_admin_panel,
            lambda msg: msg.text == "🛠️ Админ-Панель"
        )
        self.dp.register_message_handler(
            self.handle_admin_selection,
            state=Form.AdminPanel
        )
        self.dp.register_message_handler(
            self.handle_tariff_selection_for_edit,
            state=Form.SelectTariffForEdit
        )
        self.dp.register_message_handler(
            self.handle_edit_tariff_name_or_description,
            state=Form.EditTariffNameOrDescription
        )
        self.dp.register_message_handler(
            self.handle_edit_tariff_name,
            state=Form.EditTariffName
        )
        self.dp.register_message_handler(
            self.handle_edit_tariff_description,
            state=Form.EditTariffDescription
        )
        self.dp.register_message_handler(
            self.handle_edit_tariff_tokens,
            state=Form.EditTariffTokens
        )
        self.dp.register_message_handler(
            self.handle_set_tariff_tokens,
            state=Form.EditTariffTokenAmount
        )
        self.dp.register_message_handler(
            self.handle_broadcast_message,
            state=Form.BroadcastSelectTariff
        )
        self.dp.register_message_handler(
            self.handle_broadcast_text,
            state=Form.BroadcastMessageState
        )
        # Новые обработчики для управления чатами
        self.dp.register_message_handler(
            self.handle_manage_chats,
            state=Form.ManageChats
        )
        self.dp.register_message_handler(
            self.handle_add_chat,
            state=Form.AddChat
        )
        self.dp.register_message_handler(
            self.handle_delete_chat,
            state=Form.DeleteChat
        )
        # Обработчики для генерации отчёта
        self.dp.register_message_handler(
            self.handle_generate_report,
            state=Form.GenerateReport
        )
        # Регистрация обработчиков состояний FSM
        self.dp.register_message_handler(
            self.process_image_prompt,
            state=Form.ImageGenerationPrompt
        )
        self.dp.register_message_handler(
            self.process_image_size,
            state=Form.ImageGenerationSize
        )
        self.dp.register_message_handler(
            self.handle_document,
            content_types=[types.ContentType.DOCUMENT]
        )
        # Обработка аудио и голосовых сообщений
        self.dp.register_message_handler(
            self.handle_audio,
            content_types=[types.ContentType.AUDIO, types.ContentType.VOICE]
        )
        # Обработка фотографий
        self.dp.register_message_handler(
            self.handle_photo,
            content_types=[types.ContentType.PHOTO]
        )
        self.dp.register_message_handler(
            self.handle_text,
            content_types=[types.ContentType.TEXT]
        )

    async def cancel_handler(self, message: types.Message, state: FSMContext):
        # Проверка типа чата
        if message.chat.type != 'private':
            return

        # Обработка нажатия кнопки "🔙 Назад" во время состояния
        user_id = str(message.from_user.id)
        await state.finish()
        tariff_plan = self.get_user_tariff_plan(user_id)
        await message.reply("Отменено. Вы вернулись в главное меню.", reply_markup=self.get_keyboard(tariff_plan, user_id))

    def get_keyboard(self, tariff_plan: str, user_id: str):
        # Создание основной клавиатуры
        keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True)
        keyboard.row("🗑 Очистить контекст", "🤖 Выбрать модель")
        keyboard.row("🖼️ Генерация изображения", "👤 Профиль")
        if int(user_id) in config.ADMIN_IDS:
            keyboard.add("🛠️ Админ-Панель")
        return keyboard

    def get_model_keyboard(self, tariff_plan: str):
        # Создание клавиатуры выбора модели
        keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True)
        models = self.ALL_AVAILABLE_MODELS
        buttons = [types.KeyboardButton(model_name) for model_name, _, _ in models]
        keyboard.add(*buttons)
        keyboard.add("🔙 Назад")
        return keyboard

    def get_subscription_keyboard(self):
        # Клавиатура с кнопкой проверки подписки
        keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True)
        keyboard.add("🔄 Проверить подписку")
        return keyboard

    async def prompt_subscription(self, message: types.Message):
        # Проверка типа чата
        if message.chat.type != 'private':
            return

        # Просьба пользователю подписаться на канал
        keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True)
        keyboard.add("🔄 Проверить подписку")
        subscribe_button = types.InlineKeyboardButton(
            text="📢 Подписаться на канал",
            url=f"https://t.me/{config.CHANNEL_USERNAME}"
        )
        inline_keyboard = types.InlineKeyboardMarkup().add(subscribe_button)
        await message.reply(
            "Чтобы пользоваться ботом, пожалуйста, подпишитесь на наш канал.",
            reply_markup=inline_keyboard
        )
        await message.answer(
            "После подписки нажмите кнопку ниже, чтобы проверить подписку.",
            reply_markup=keyboard
        )

    async def check_subscription_button_handler(self, message: types.Message):
        # Обработка нажатия кнопки "Проверить подписку"
        await self.cmd_start(message)

    async def cmd_start(self, message: types.Message):
        # Проверка типа чата
        if message.chat.type != 'private':
            return

        # Обработка команды /start
        user_id = str(message.from_user.id)
        telegram_user_id = message.from_user.id

        # Проверка подписки
        is_subscribed = await self.check_user_subscription(telegram_user_id)
        self.set_user_subscription(user_id, is_subscribed)

        if not is_subscribed:
            await self.prompt_subscription(message)
        else:
            # Проверяем и обновляем тарифный план пользователя
            await self.check_and_update_tariff_plan(user_id)
            await self.refresh_user_tokens(user_id)
            tariff_plan = self.get_user_tariff_plan(user_id)
            keyboard = self.get_keyboard(tariff_plan, user_id)

            welcome_text = (
                "👋 <b>Привет!</b>\n\n"
                "Я бот для общения с AI моделями.\n\n"
                "Выберите модель для общения и отправляйте сообщения.\n\n"
                "Чтобы сменить модель, нажмите <b>🤖 Выбрать модель</b>.\n"
                "Чтобы очистить контекст беседы, нажмите <b>🗑 Очистить контекст</b>.\n"
                "Для доступа к профилю нажмите <b>👤 Профиль</b>.\n"
                "Для генерации изображения нажмите <b>🖼️ Генерация изображения</b>."
            )

            await message.reply(welcome_text, reply_markup=keyboard, parse_mode=types.ParseMode.HTML)

    async def show_model_selection(self, message: types.Message):
        # Проверка типа чата
        if message.chat.type != 'private':
            return

        # Показать выбор моделей
        user_id = str(message.from_user.id)
        tariff_plan = self.get_user_tariff_plan(user_id)
        keyboard = self.get_model_keyboard(tariff_plan)
        await message.reply(
            "Выберите модель для общения:",
            reply_markup=keyboard
        )

    async def process_model_selection(self, message: types.Message):
        # Проверка типа чата
        if message.chat.type != 'private':
            return

        # Обработка выбора модели
        user_id = str(message.from_user.id)
        telegram_user_id = message.from_user.id

        # Проверка подписки
        is_subscribed = await self.check_user_subscription(telegram_user_id)
        self.set_user_subscription(user_id, is_subscribed)

        if not is_subscribed:
            await self.prompt_subscription(message)
            return

        # Проверяем и обновляем тарифный план пользователя
        await self.check_and_update_tariff_plan(user_id)
        await self.refresh_user_tokens(user_id)
        tariff_plan = self.get_user_tariff_plan(user_id)

        if message.text == "🔙 Назад":
            await message.reply("Вы вернулись в главное меню.", reply_markup=self.get_keyboard(tariff_plan, user_id))
            return

        available_models = [model[0] for model in self.ALL_AVAILABLE_MODELS]

        model_name = message.text
        if model_name not in available_models:
            await message.reply("Неверный выбор модели. Пожалуйста, выберите модель из списка.")
            return

        model_id = None
        for name, id, _ in self.ALL_AVAILABLE_MODELS:
            if name == model_name:
                model_id = id
                break

        if not model_id:
            await message.reply("Неверный выбор модели. Пожалуйста, выберите модель из списка.")
            return

        # Сохранение выбора модели пользователя
        self.set_user_model(user_id, model_id)

        # Очистка контекста беседы при смене модели
        self.conversations.pop(user_id, None)

        await message.reply(f"Выбрана модель: {model_name}", reply_markup=self.get_keyboard(tariff_plan, user_id))

    async def handle_clear_context(self, message: types.Message):
        # Проверка типа чата
        if message.chat.type != 'private':
            return

        # Очистка контекста беседы
        user_id = str(message.from_user.id)
        telegram_user_id = message.from_user.id

        # Проверка подписки
        is_subscribed = await self.check_user_subscription(telegram_user_id)
        self.set_user_subscription(user_id, is_subscribed)

        if not is_subscribed:
            await self.prompt_subscription(message)
            return

        self.conversations.pop(user_id, None)
        tariff_plan = self.get_user_tariff_plan(user_id)
        await message.reply(
            "Контекст очищен! Можете начать новый разговор.",
            reply_markup=self.get_keyboard(tariff_plan, user_id)
        )

    async def show_profile(self, message: types.Message):
        # Проверка типа чата
        if message.chat.type != 'private':
            return

        # Отображение профиля пользователя
        user_id = str(message.from_user.id)
        telegram_user_id = message.from_user.id

        # Проверка подписки и обновление тарифного плана
        is_subscribed = await self.check_user_subscription(telegram_user_id)
        self.set_user_subscription(user_id, is_subscribed)
        await self.check_and_update_tariff_plan(user_id)
        await self.refresh_user_tokens(user_id)

        tariff_plan = self.get_user_tariff_plan(user_id)
        tokens_available = self.get_user_tokens(user_id)
        last_refresh = self.get_last_token_refresh(user_id)
        now = datetime.now()
        if last_refresh:
            next_refresh_date = last_refresh + timedelta(days=1)
            hours_left = int((next_refresh_date - now).total_seconds() // 3600)
            if hours_left < 0:
                hours_left = 0
        else:
            hours_left = 24

        if tokens_available == -1:
            tokens_text = "∞"
        else:
            tokens_text = f"{tokens_available:,}".replace(",", " ")

        tariff_info = self.get_tariff_info(tariff_plan)
        if tariff_info:
            tariff_name = tariff_info['name']
            tariff_description = tariff_info['description']
        else:
            tariff_name = tariff_plan
            tariff_description = "Нет описания для этого тарифа."

        if tokens_available == -1:
            refresh_text = ""
        else:
            if hours_left == 0:
                refresh_text = "Обновление токенов: <b>скоро</b>"
            else:
                refresh_text = f"Обновление токенов через: <b>{hours_left} часов</b>"

        profile_text = (
            f"👤 <b>Профиль пользователя</b>\n\n"
            f"Ваш тариф: <b>{tariff_name}</b>\n\n"
            f"<b>Описание тарифа:</b>\n{tariff_description}\n\n"
            f"Токенов осталось: <b>{tokens_text}</b>\n"
            f"{refresh_text}"
        )

        await message.reply(profile_text, reply_markup=self.get_keyboard(tariff_plan, user_id), parse_mode=types.ParseMode.HTML)

    async def start_image_generation(self, message: types.Message):
        # Проверка типа чата
        if message.chat.type != 'private':
            return

        # Начало процесса генерации изображения
        user_id = str(message.from_user.id)
        telegram_user_id = message.from_user.id

        # Проверка подписки
        is_subscribed = await self.check_user_subscription(telegram_user_id)
        self.set_user_subscription(user_id, is_subscribed)

        if not is_subscribed:
            await self.prompt_subscription(message)
            return

        await self.check_and_update_tariff_plan(user_id)
        await self.refresh_user_tokens(user_id)
        tariff_plan = self.get_user_tariff_plan(user_id)

        # Проверяем, есть ли у пользователя достаточно токенов (100 токенов за генерацию)
        tokens_available = self.get_user_tokens(user_id)
        if tokens_available != -1 and tokens_available < 100:
            await message.reply("У вас недостаточно токенов для генерации изображения.")
            return

        await Form.ImageGenerationPrompt.set()
        keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True)
        keyboard.add("🔙 Назад")
        await message.reply(
            "Пожалуйста, введите описание изображения, которое вы хотите сгенерировать:",
            reply_markup=keyboard
        )

    async def process_image_prompt(self, message: types.Message, state: FSMContext):
        # Проверка типа чата
        if message.chat.type != 'private':
            return

        # Обработка описания изображения
        user_id = str(message.from_user.id)
        await self.check_and_update_tariff_plan(user_id)
        await self.refresh_user_tokens(user_id)
        tariff_plan = self.get_user_tariff_plan(user_id)

        if message.text == "🔙 Назад":
            await state.finish()
            await message.reply("Вы вернулись в главное меню.", reply_markup=self.get_keyboard(tariff_plan, user_id))
            return

        prompt = message.text

        # Переводим описание на английский с помощью GPT-4
        translated_prompt = await self.translate_prompt(prompt, user_id)

        if not translated_prompt:
            await message.reply("Не удалось перевести описание на английский.")
            await state.finish()
            return

        # Сохраняем переведенное описание в состоянии
        await state.update_data(prompt=translated_prompt)

        # Предлагаем выбрать соотношение сторон
        keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True)
        keyboard.add("1:1", "3:4", "16:9", "9:16")
        keyboard.add("🔙 Назад")
        await Form.next()
        await message.reply("Пожалуйста, выберите соотношение сторон изображения:", reply_markup=keyboard)

    async def process_image_size(self, message: types.Message, state: FSMContext):
        # Проверка типа чата
        if message.chat.type != 'private':
            return

        # Обработка выбора соотношения сторон
        user_id = str(message.from_user.id)
        await self.check_and_update_tariff_plan(user_id)
        await self.refresh_user_tokens(user_id)
        tariff_plan = self.get_user_tariff_plan(user_id)

        if message.text == "🔙 Назад":
            await state.finish()
            await message.reply("Вы вернулись в главное меню.", reply_markup=self.get_keyboard(tariff_plan, user_id))
            return

        size = message.text
        if size not in ["1:1", "3:4", "16:9", "9:16"]:
            await message.reply("Пожалуйста, выберите соотношение сторон из предложенных вариантов.")
            return

        await state.update_data(size=size)

        # Проверяем, есть ли у пользователя достаточно токенов (100 токенов за генерацию)
        tokens_available = self.get_user_tokens(user_id)
        if tokens_available != -1 and tokens_available < 100:
            await message.reply("У вас недостаточно токенов для генерации изображения.")
            await state.finish()
            return

        # Списываем 100 токенов
        if tokens_available != -1:
            self.set_user_tokens(user_id, tokens_available - 100)

        # Получаем данные из состояния
        data = await state.get_data()
        prompt = data['prompt']
        size = data['size']

        # Показываем, что бот набирает ответ
        await self.bot.send_chat_action(message.chat.id, types.ChatActions.UPLOAD_PHOTO)

        waiting_message = await message.reply("Генерирую изображение...")
        self.waiting_messages[user_id] = waiting_message

        try:
            # Используем ImageAPI для генерации изображения
            images = await asyncio.to_thread(
                self.image_api.generate_images,
                prompt=prompt,
                n=1,
                size=size
            )
            if images:
                for image_url in images:
                    await message.reply_photo(photo=image_url)
            else:
                await message.reply("Не удалось сгенерировать изображение.")
        except Exception as e:
            logger.error(f"Ошибка при генерации изображения: {str(e)}", exc_info=True)
            await message.reply(f"Произошла ошибка при генерации изображения: {str(e)}")
        finally:
            if user_id in self.waiting_messages:
                await self.waiting_messages[user_id].delete()
                del self.waiting_messages[user_id]
            # Завершаем состояние и возвращаем основную клавиатуру
            await state.finish()
            await message.reply("Вы можете продолжить общение.", reply_markup=self.get_keyboard(tariff_plan, user_id))

    async def translate_prompt(self, prompt: str, user_id: str) -> str:
        # Переводим текст на английский с помощью GPT-4
        try:
            model = 'gpt-4o-mini'
            api_name = self.MODEL_API_MAPPING.get(model)
            api_client = self.api_clients.get(api_name)
            if not api_client:
                return None

            response = await asyncio.to_thread(
                api_client.send_message,
                message=f"Please translate the following text to English:\n\n{prompt}",
                conversation_id=None,
                models=[model]
            )

            if not response:
                return None

            data = response.get('data')
            if not data:
                return None

            messages = data.get('messages')
            if not messages:
                return None

            assistant_message = next(
                (msg for msg in reversed(messages) if msg.get('role') == 'assistant'),
                None
            )

            if not assistant_message:
                return None

            content = assistant_message.get('content')
            return content.strip() if content else None
        except Exception as e:
            logger.error(f"Ошибка при переводе описания: {str(e)}", exc_info=True)
            return None

    async def handle_document(self, message: types.Message):
        # Проверка типа чата
        if message.chat.type != 'private':
            return

        # Обработка документов
        if not message.document:
            return

        user_id = str(message.from_user.id)
        telegram_user_id = message.from_user.id

        # Проверка подписки
        is_subscribed = await self.check_user_subscription(telegram_user_id)
        self.set_user_subscription(user_id, is_subscribed)

        if not is_subscribed:
            await self.prompt_subscription(message)
            return

        # Проверка занятости пользователя
        if user_id in self.user_tasks:
            await message.reply("Пожалуйста, дождитесь ответа на предыдущее сообщение.")
            return

        # Проверяем и обновляем тарифный план пользователя
        await self.check_and_update_tariff_plan(user_id)
        await self.refresh_user_tokens(user_id)

        # Проверяем, есть ли у пользователя достаточно токенов
        tokens_available = self.get_user_tokens(user_id)
        if tokens_available == 0:
            await message.reply("У вас закончились токены. Подождите до обновления лимита или измените тарифный план.")
            return

        # Создаем задачу для обработки документа
        task = asyncio.create_task(self._process_document(message))
        self.user_tasks[user_id] = task

    async def _process_document(self, message: types.Message):
        # Вспомогательная функция для обработки документа
        user_id = str(message.from_user.id)
        try:
            file_name = message.document.file_name or "document"
            content = await process_file_content(self.bot, message.document.file_id, file_name)

            prompt = f"Анализ файла {file_name}:\n\n"
            if message.caption:
                prompt += f"Подпись к файлу: {message.caption}\n\n"
            prompt += f"Содержимое:\n{content}"

            # Считаем количество токенов и обновляем баланс
            tokens_used = len(prompt)
            current_tokens = self.get_user_tokens(user_id)
            if current_tokens != -1:
                if tokens_used > current_tokens:
                    await message.reply("У вас недостаточно токенов для анализа этого файла.")
                    return
                self.set_user_tokens(user_id, current_tokens - tokens_used)

            await self.process_message(message, prompt)
        finally:
            # Удаляем задачу из списка после завершения
            if user_id in self.user_tasks:
                del self.user_tasks[user_id]

    async def handle_audio(self, message: types.Message):
        # Проверка типа чата
        if message.chat.type != 'private':
            return

        # Обработка аудио и голосовых сообщений
        user_id = str(message.from_user.id)
        telegram_user_id = message.from_user.id

        # Проверка подписки
        is_subscribed = await self.check_user_subscription(telegram_user_id)
        self.set_user_subscription(user_id, is_subscribed)

        if not is_subscribed:
            await self.prompt_subscription(message)
            return

        # Проверка занятости пользователя
        if user_id in self.user_tasks:
            await message.reply("Пожалуйста, дождитесь ответа на предыдущее сообщение.")
            return

        # Проверяем и обновляем тарифный план пользователя
        await self.check_and_update_tariff_plan(user_id)
        await self.refresh_user_tokens(user_id)

        # Проверяем, есть ли у пользователя достаточно токенов
        tokens_available = self.get_user_tokens(user_id)
        if tokens_available == 0:
            await message.reply("У вас закончились токенов. Подождите до обновления лимита или измените тарифный план.")
            return

        # Создаем задачу для обработки аудио
        task = asyncio.create_task(self._process_audio(message))
        self.user_tasks[user_id] = task

    async def _process_audio(self, message: types.Message):
        # Вспомогательная функция для обработки аудио
        user_id = str(message.from_user.id)
        try:
            await self.audio_processor.process_audio_message(message, self.process_message)
        finally:
            # Удаляем задачу из списка после завершения
            if user_id in self.user_tasks:
                del self.user_tasks[user_id]

    async def handle_photo(self, message: types.Message):
        # Проверка типа чата
        if message.chat.type != 'private':
            return

        # Обработка фотографий
        user_id = str(message.from_user.id)
        telegram_user_id = message.from_user.id

        # Проверка подписки
        is_subscribed = await self.check_user_subscription(telegram_user_id)
        self.set_user_subscription(user_id, is_subscribed)

        if not is_subscribed:
            await self.prompt_subscription(message)
            return

        # Проверка занятости пользователя
        if user_id in self.user_tasks:
            await message.reply("Пожалуйста, дождитесь ответа на предыдущее сообщение.")
            return

        # Проверяем и обновляем тарифный план пользователя
        await self.check_and_update_tariff_plan(user_id)
        await self.refresh_user_tokens(user_id)

        # Проверяем, есть ли у пользователя достаточно токенов
        tokens_available = self.get_user_tokens(user_id)
        if tokens_available == 0:
            await message.reply("У вас закончились токенов. Подождите до обновления лимита или измените тарифный план.")
            return

        # Создаем задачу для обработки фотографии
        task = asyncio.create_task(self._process_photo(message))
        self.user_tasks[user_id] = task

    async def _process_photo(self, message: types.Message):
        # Вспомогательная функция для обработки фотографии
        user_id = str(message.from_user.id)
        try:
            # Получаем файл изображения
            photo = message.photo[-1]  # Используем изображение с максимальным разрешением
            file_info = await self.bot.get_file(photo.file_id)
            image_url = f"https://api.telegram.org/file/bot{config.BOT_TOKEN}/{file_info.file_path}"

            # Подготавливаем контекст для gpt-4o
            prompt = message.caption or "Изображение для анализа."

            # Сохраняем контекст
            await self.process_message(message, prompt, image_url=image_url)
        finally:
            # Удаляем задачу из списка после завершения
            if user_id in self.user_tasks:
                del self.user_tasks[user_id]

    async def handle_text(self, message: types.Message):
        # Проверка типа чата
        if message.chat.type != 'private':
            return

        # Обработка текстовых сообщений
        user_id = str(message.from_user.id)
        telegram_user_id = message.from_user.id

        control_messages = [
            "🗑 Очистить контекст",
            "🤖 Выбрать модель",
            "🖼️ Генерация изображения",
            "👤 Профиль",
            "🛠️ Админ-Панель",
            "🔙 Назад",
            "🔄 Проверить подписку"
        ] + [model[0] for model in self.ALL_AVAILABLE_MODELS]

        if message.text in control_messages:
            return

        # Проверка подписки
        is_subscribed = await self.check_user_subscription(telegram_user_id)
        self.set_user_subscription(user_id, is_subscribed)

        if not is_subscribed:
            await self.prompt_subscription(message)
            return

        # Проверка занятости пользователя
        if user_id in self.user_tasks:
            await message.reply("Пожалуйста, дождитесь ответа на предыдущее сообщение.")
            return

        # Проверяем и обновляем тарифный план пользователя
        await self.check_and_update_tariff_plan(user_id)
        await self.refresh_user_tokens(user_id)

        # Проверяем, есть ли у пользователя достаточно токенов
        tokens_available = self.get_user_tokens(user_id)
        if tokens_available == 0:
            await message.reply("У вас закончились токенов. Подождите до обновления лимита или измените тарифный план.")
            return

        # Передаем обработку в отдельную функцию
        task = asyncio.create_task(self._process_text_message(message))
        self.user_tasks[user_id] = task

    async def _process_text_message(self, message: types.Message):
        # Вспомогательная функция для обработки текстового сообщения
        user_id = str(message.from_user.id)
        try:
            await self.process_message(message, message.text)
        finally:
            # Удаляем задачу из списка после завершения
            if user_id in self.user_tasks:
                del self.user_tasks[user_id]

    async def process_message(self, message: types.Message, text: str, image_url: str = None):
        # Проверка типа чата
        if message.chat.type != 'private':
            return

        # Обработка текстовых сообщений с контекстом
        user_id = str(message.from_user.id)
        telegram_user_id = message.from_user.id

        # Показываем, что бот набирает ответ
        await self.bot.send_chat_action(message.chat.id, types.ChatActions.TYPING)

        waiting_message = await message.reply("Пишу для вас ответ...")
        self.waiting_messages[user_id] = waiting_message

        try:
            # Проверяем и обновляем тарифный план пользователя
            await self.check_and_update_tariff_plan(user_id)
            await self.refresh_user_tokens(user_id)
            tariff_plan = self.get_user_tariff_plan(user_id)

            # Проверяем, есть ли у пользователя достаточно токенов
            tokens_available = self.get_user_tokens(user_id)
            if tokens_available == 0:
                await message.reply("У вас закончились токенов. Подождите до обновления лимита или измените тарифный план.")
                return

            # Получение выбранной модели пользователя
            model = self.get_user_model(user_id)
            # Получение API клиента
            api_name = self.MODEL_API_MAPPING.get(model)
            api_client = self.api_clients.get(api_name)

            if not api_client:
                await message.reply("Ошибка в настройке API. Пожалуйста, свяжитесь с администратором.")
                return

            # Получение предыдущего контекста для пользователя
            conversation = self.conversations.get(user_id, [])

            if api_name == 'sparrow':
                # Используем SparrowAPI
                conversation_id = self.conversations.get(user_id, {}).get('conversation_id')
                response = await asyncio.to_thread(
                    api_client.send_message,
                    message=text,
                    conversation_id=conversation_id,
                    models=[model]
                )

                if not response or 'data' not in response:
                    await message.reply("Получен некорректный ответ от API")
                    return

                data = response['data']
                messages = data.get('messages', [])

                if not messages:
                    await message.reply("Не получено сообщений от API")
                    return

                if 'conversationId' in data:
                    self.conversations.setdefault(user_id, {})['conversation_id'] = data['conversationId']

                assistant_message = next(
                    (msg for msg in reversed(messages) if msg.get('role') == 'assistant'),
                    None
                )

                if not assistant_message:
                    await message.reply("Не получен ответ от ассистента")
                    return

                content = assistant_message.get('content')
                if not content and 'contents' in assistant_message:
                    content = next(
                        (c.get('content') for c in assistant_message['contents']
                         if c.get('model') == model),
                        None
                    )

                if not content:
                    await message.reply("Не удалось получить текст ответа")
                    return

                # Считаем количество токенов и обновляем баланс
                tokens_used = len(content)
                current_tokens = self.get_user_tokens(user_id)
                if current_tokens != -1:
                    if tokens_used > current_tokens:
                        await message.reply("У вас недостаточно токенов для этого ответа.")
                        return
                    self.set_user_tokens(user_id, current_tokens - tokens_used)

                await send_long_message(message, content)

            elif api_name == 'gpt4o':
                # Используем GPT4OAPI
                if image_url:
                    # Если есть изображение, формируем контент специальным образом
                    content = [
                        {"text": text, "type": "text"},
                        {"type": "image_url", "image_url": {"url": image_url}}
                    ]

                    item = {
                        "role": "user",
                        "content": content
                    }

                    data = {
                        "model": model,
                        "item": item
                    }
                else:
                    # Иначе объединяем контекст и отправляем как текст
                    conversation.append(f"Пользователь: {text}")
                    context_text = build_context(conversation)

                    item = {
                        "role": "user",
                        "content": context_text
                    }

                    data = {
                        "model": model,
                        "item": item
                    }

                # Отправка запроса к GPT4OAPI
                response = await asyncio.to_thread(
                    api_client.send_message,
                    data=data
                )

                if not response:
                    await message.reply("Получен некорректный ответ от API")
                    return

                # Извлечение ответа ассистента
                choices = response.get('choices')
                if not choices:
                    await message.reply("Не удалось получить ответ от ассистента")
                    return

                assistant_message = choices[0].get('message')
                if not assistant_message:
                    await message.reply("Не удалось получить ответ от ассистента")
                    return

                assistant_content = assistant_message.get('content')
                if assistant_content:
                    if image_url:
                        # При обработке изображения сохраняем контент как есть
                        conversation.append(f"Пользователь: [Изображение]")
                        conversation.append(f"Ассистент: {assistant_content}")
                    else:
                        # Добавляем ответ ассистента в контекст
                        conversation.append(f"Ассистент: {assistant_content}")

                    self.conversations[user_id] = conversation

                    # Считаем количество токенов и обновляем баланс
                    tokens_used = len(str(assistant_content))
                    current_tokens = self.get_user_tokens(user_id)
                    if current_tokens != -1:
                        if tokens_used > current_tokens:
                            await message.reply("У вас недостаточно токенов для этого ответа.")
                            return
                        self.set_user_tokens(user_id, current_tokens - tokens_used)

                    # Отправка ответа пользователю
                    if isinstance(assistant_content, list):
                        # Обрабатываем список содержимого
                        for content_item in assistant_content:
                            if content_item.get('type') == 'text':
                                text = content_item.get('text')
                                await send_long_message(message, text)
                            elif content_item.get('type') == 'image_url':
                                image_data = content_item.get('image_url', {})
                                image_url = image_data.get('url')
                                if image_url:
                                    await message.reply_photo(photo=image_url)
                    else:
                        # Если контент представлен в виде текста
                        await send_long_message(message, assistant_content)
                else:
                    await message.reply("Не удалось получить текст ответа")

            elif api_name == 'forget':
                # Используем ForgetAPI
                conversation.append({"role": "user", "content": text})
                conversation = conversation[-10:]  # Ограничиваем контекст

                # Отправка запроса к ForgetAPI
                response = await asyncio.to_thread(
                    api_client.send_message,
                    model=model,
                    messages=conversation,
                    temperature=0.7,
                    max_tokens=10000
                )

                if not response:
                    await message.reply("Получен некорректный ответ от API")
                    return

                # Извлечение ответа ассистента
                choices = response.get('choices')
                if not choices:
                    await message.reply("Не удалось получить ответ от ассистента")
                    return

                assistant_message = choices[0].get('message')
                if not assistant_message:
                    await message.reply("Не удалось получить ответ от ассистента")
                    return

                content = assistant_message.get('content')
                if content:
                    conversation.append({"role": "assistant", "content": content})
                    self.conversations[user_id] = conversation

                    # Считаем количество токенов и обновляем баланс
                    tokens_used = len(content)
                    current_tokens = self.get_user_tokens(user_id)
                    if current_tokens != -1:
                        if tokens_used > current_tokens:
                            await message.reply("У вас недостаточно токенов для этого ответа.")
                            return
                        self.set_user_tokens(user_id, current_tokens - tokens_used)

                    # Отправка ответа пользователю
                    await send_long_message(message, content)
                else:
                    await message.reply("Не удалось получить текст ответа")
            else:
                await message.reply("Неизвестная модель или API. Пожалуйста, выберите модель из списка.")
                return

        except Exception as e:
            logger.error(f"Ошибка в process_message: {str(e)}", exc_info=True)
            await message.reply(f"Произошла ошибка при обработке вашего сообщения: {str(e)}")
        finally:
            if user_id in self.waiting_messages:
                await self.waiting_messages[user_id].delete()
                del self.waiting_messages[user_id]

    async def start(self):
        # Запуск бота
        logger.info("Запуск бота...")
        try:
            await self.dp.start_polling()
        except (KeyboardInterrupt, SystemExit):
            logger.info("Бот остановлен")
        except Exception as e:
            logger.error(f"Ошибка запуска бота: {str(e)}", exc_info=True)
            raise
        finally:
            # Закрываем соединение с базой данных при остановке бота
            self.db_connection.close()

    async def check_user_subscription(self, user_id: int) -> bool:
        # Проверка подписки пользователя на канал
        try:
            chat_member = await self.bot.get_chat_member(chat_id='@' + config.CHANNEL_USERNAME, user_id=user_id)
            if chat_member.status in ['member', 'administrator', 'creator']:
                return True
            else:
                return False
        except Exception as e:
            logger.error(f"Ошибка при проверке подписки для пользователя {user_id}: {e}")
            return False

    # ----- Обработчики для Админ-Панели -----

    async def show_admin_panel(self, message: types.Message):
        # Проверка, что пользователь является администратором
        if message.from_user.id not in config.ADMIN_IDS:
            return

        await Form.AdminPanel.set()
        keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True)
        keyboard.add("Изменить тариф")
        keyboard.add("Изменить токены тарифа")
        keyboard.add("Сделать рассылку")
        keyboard.add("Управление чатами")
        keyboard.add("Сгенерировать отчёт")
        keyboard.add("🔙 Назад")
        await message.reply("Добро пожаловать в Админ-Панель. Выберите действие:", reply_markup=keyboard)

    async def handle_admin_selection(self, message: types.Message, state: FSMContext):
        if message.from_user.id not in config.ADMIN_IDS:
            await state.finish()
            return

        if message.text == "Изменить тариф":
            # Переходим к выбору тарифа для редактирования
            await Form.SelectTariffForEdit.set()
            keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True)
            tariffs = ['free', 'premium', 'admin']
            keyboard.add(*tariffs)
            keyboard.add("🔙 Назад")
            await message.reply("Выберите тариф для редактирования:", reply_markup=keyboard)
        elif message.text == "Изменить токены тарифа":
            # Переходим к выбору тарифа для изменения токенов
            await Form.EditTariffTokens.set()
            keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True)
            tariffs = ['free', 'premium', 'admin']
            keyboard.add(*tariffs)
            keyboard.add("🔙 Назад")
            await message.reply("Выберите тариф, для которого хотите изменить количество ежедневных токенов:", reply_markup=keyboard)
        elif message.text == "Сделать рассылку":
            # Переходим к рассылке сообщений
            await Form.BroadcastSelectTariff.set()
            keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True)
            tariffs = ['free', 'premium', 'admin', 'Все']
            keyboard.add(*tariffs)
            keyboard.add("🔙 Назад")
            await message.reply("Выберите тариф, пользователям которого хотите отправить сообщение:", reply_markup=keyboard)
        elif message.text == "Управление чатами":
            # Переходим к управлению чатами
            await Form.ManageChats.set()
            keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True)
            keyboard.add("Добавить чат")
            keyboard.add("Удалить чат")
            keyboard.add("Показать чаты")
            keyboard.add("🔙 Назад")
            await message.reply("Выберите действие с чатами:", reply_markup=keyboard)
        elif message.text == "Сгенерировать отчёт":
            # Генерация отчёта
            await Form.GenerateReport.set()
            await self.generate_report(message)
            await state.finish()
        elif message.text == "🔙 Назад":
            await state.finish()
            await self.cmd_start(message)
        else:
            await message.reply("Пожалуйста, выберите действие из списка.")

    async def handle_manage_chats(self, message: types.Message, state: FSMContext):
        if message.text == "Добавить чат":
            await Form.AddChat.set()
            await message.reply("Пожалуйста, отправьте ID чата, который хотите добавить в премиум-чаты:")
        elif message.text == "Удалить чат":
            await Form.DeleteChat.set()
            await message.reply("Пожалуйста, отправьте ID чата, который хотите удалить из премиум-чатов:")
        elif message.text == "Показать чаты":
            # Показываем список премиум-чатов
            self.db_cursor.execute('SELECT chat_id FROM premium_chats')
            premium_chat_ids = [row[0] for row in self.db_cursor.fetchall()]
            chat_list = "\n".join([str(chat_id) for chat_id in premium_chat_ids])
            await message.reply(f"Список премиум-чатов:\n{chat_list}")
        elif message.text == "🔙 Назад":
            await Form.AdminPanel.set()
            await self.show_admin_panel(message)
        else:
            await message.reply("Пожалуйста, выберите действие из списка.")

    async def handle_add_chat(self, message: types.Message, state: FSMContext):
        try:
            chat_id = int(message.text)
            self.db_cursor.execute('INSERT INTO premium_chats (chat_id) VALUES (?)', (chat_id,))
            self.db_connection.commit()
            await message.reply(f"Чат {chat_id} успешно добавлен в премиум-чаты.")
            await state.finish()
            await self.show_admin_panel(message)
        except Exception as e:
            await message.reply(f"Ошибка при добавлении чата: {e}")
            await state.finish()
            await self.show_admin_panel(message)

    async def handle_delete_chat(self, message: types.Message, state: FSMContext):
        try:
            chat_id = int(message.text)
            self.db_cursor.execute('DELETE FROM premium_chats WHERE chat_id = ?', (chat_id,))
            self.db_connection.commit()
            await message.reply(f"Чат {chat_id} успешно удалён из премиум-чатов.")
            await state.finish()
            await self.show_admin_panel(message)
        except Exception as e:
            await message.reply(f"Ошибка при удалении чата: {e}")
            await state.finish()
            await self.show_admin_panel(message)

    async def generate_report(self, message: types.Message):
        # Генерация отчёта в формате xlsx
        await message.reply("Генерирую отчёт...")

        workbook = xlsxwriter.Workbook('report.xlsx')
        worksheet = workbook.add_worksheet()

        # Заголовки столбцов
        headers = ['Группа', 'Тариф', 'ID пользователя', 'Использовано токенов']
        for col_num, header in enumerate(headers):
            worksheet.write(0, col_num, header)

        # Получаем список пользователей
        self.db_cursor.execute('SELECT user_id, tariff_plan, tokens_available, group_association FROM users')
        users = self.db_cursor.fetchall()

        # Получаем список премиум-чатов
        self.db_cursor.execute('SELECT chat_id FROM premium_chats')
        premium_chat_ids = [str(row[0]) for row in self.db_cursor.fetchall()]

        # Группа, при старте на которую нужно подписаться
        start_group = '@' + config.CHANNEL_USERNAME

        # Заполняем данные
        row = 1
        for user_id, tariff_plan, tokens_available, group_association in users:
            if group_association in premium_chat_ids:
                group_name = f"Чат {group_association}"
            elif group_association == "None":
                group_name = start_group
            else:
                group_name = group_association

            worksheet.write(row, 0, group_name)
            worksheet.write(row, 1, tariff_plan)
            worksheet.write(row, 2, user_id)
            tokens_used = self.get_tariff_info(tariff_plan)['daily_tokens'] - tokens_available if tokens_available != -1 else 0
            worksheet.write(row, 3, tokens_used)
            row += 1

        workbook.close()

        with open('report.xlsx', 'rb') as f:
            await self.bot.send_document(chat_id=message.chat.id, document=f)

    async def handle_generate_report(self, message: types.Message, state: FSMContext):
        # Этот обработчик фактически не используется, так как генерация происходит сразу
        pass

    async def handle_tariff_selection_for_edit(self, message: types.Message, state: FSMContext):
        if message.text == "🔙 Назад":
            await state.finish()
            await self.show_admin_panel(message)
            return
        tariff_id = message.text
        if tariff_id not in ['free', 'premium', 'admin']:
            await message.reply("Пожалуйста, выберите тариф из списка.")
            return
        await state.update_data(tariff_id=tariff_id)
        await Form.EditTariffNameOrDescription.set()
        keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True)
        keyboard.add("Изменить название", "Изменить описание")
        keyboard.add("🔙 Назад")
        await message.reply("Выберите, что вы хотите изменить:", reply_markup=keyboard)

    async def handle_edit_tariff_name_or_description(self, message: types.Message, state: FSMContext):
        if message.text == "🔙 Назад":
            await Form.SelectTariffForEdit.set()
            keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True)
            tariffs = ['free', 'premium', 'admin']
            keyboard.add(*tariffs)
            keyboard.add("🔙 Назад")
            await message.reply("Выберите тариф для редактирования:", reply_markup=keyboard)
            return
        data = await state.get_data()
        tariff_id = data.get('tariff_id')

        if message.text == "Изменить название":
            await Form.EditTariffName.set()
            await message.reply("Введите новое название тарифа:")
        elif message.text == "Изменить описание":
            await Form.EditTariffDescription.set()
            await message.reply("Введите новое описание тарифа:")
        else:
            await message.reply("Пожалуйста, выберите действие из списка.")

    async def handle_edit_tariff_name(self, message: types.Message, state: FSMContext):
        new_name = message.text
        data = await state.get_data()
        tariff_id = data.get('tariff_id')
        self.set_tariff_info(tariff_id, name=new_name)
        await message.reply(f"Название тарифа '{tariff_id}' успешно изменено на '{new_name}'.")
        await state.finish()
        await self.show_admin_panel(message)

    async def handle_edit_tariff_description(self, message: types.Message, state: FSMContext):
        new_description = message.text
        data = await state.get_data()
        tariff_id = data.get('tariff_id')
        self.set_tariff_info(tariff_id, description=new_description)
        await message.reply(f"Описание тарифа '{tariff_id}' успешно изменено.")
        await state.finish()
        await self.show_admin_panel(message)

    async def handle_edit_tariff_name(self, message: types.Message, state: FSMContext):
        new_name = message.text
        data = await state.get_data()
        tariff_id = data.get('tariff_id')
        self.set_tariff_info(tariff_id, name=new_name)
        await message.reply(f"Название тарифа '{tariff_id}' успешно изменено на '{new_name}'.")
        await state.finish()
        await self.show_admin_panel(message)

    async def handle_edit_tariff_description(self, message: types.Message, state: FSMContext):
        new_description = message.text
        data = await state.get_data()
        tariff_id = data.get('tariff_id')
        self.set_tariff_info(tariff_id, description=new_description)
        await message.reply(f"Описание тарифа '{tariff_id}' успешно изменено.")
        await state.finish()
        await self.show_admin_panel(message)

    async def handle_edit_tariff_tokens(self, message: types.Message, state: FSMContext):
        if message.text == "🔙 Назад":
            await state.finish()
            await self.show_admin_panel(message)
            return
        tariff_id = message.text
        if tariff_id not in ['free', 'premium', 'admin']:
            await message.reply("Пожалуйста, выберите тариф из списка.")
            return
        await state.update_data(tariff_id=tariff_id)
        await Form.EditTariffTokenAmount.set()
        await message.reply("Введите новое количество ежедневных токенов (введите -1 для неограниченного количества):")

    async def handle_set_tariff_tokens(self, message: types.Message, state: FSMContext):
        try:
            new_token_amount = int(message.text)
            data = await state.get_data()
            tariff_id = data.get('tariff_id')
            self.set_tariff_info(tariff_id, daily_tokens=new_token_amount)
            await message.reply(f"Количество ежедневных токенов для тарифа '{tariff_id}' успешно изменено на {new_token_amount}.")
            await state.finish()
            await self.show_admin_panel(message)
        except ValueError:
            await message.reply("Пожалуйста, введите корректное число.")

    async def handle_broadcast_message(self, message: types.Message, state: FSMContext):
        if message.text == "🔙 Назад":
            await state.finish()
            await self.show_admin_panel(message)
            return
        tariff_id = message.text
        if tariff_id not in ['free', 'premium', 'admin', 'Все']:
            await message.reply("Пожалуйста, выберите тариф из списка.")
            return
        await state.update_data(tariff_id=tariff_id)
        await Form.BroadcastMessageState.set()
        await message.reply("Введите сообщение для рассылки:")

    async def handle_broadcast_text(self, message: types.Message, state: FSMContext):
        broadcast_text = message.text
        data = await state.get_data()
        tariff_id = data.get('tariff_id')

        # Получаем список пользователей
        if tariff_id == 'Все':
            self.db_cursor.execute('SELECT user_id FROM users')
        else:
            self.db_cursor.execute('SELECT user_id FROM users WHERE tariff_plan = ?', (tariff_id,))

        user_ids = [row[0] for row in self.db_cursor.fetchall()]
        success_count = 0
        for user_id in user_ids:
            try:
                await self.bot.send_message(chat_id=int(user_id), text=broadcast_text)
                success_count += 1
            except Exception as e:
                logger.error(f"Не удалось отправить сообщение пользователю {user_id}: {e}")

        await message.reply(f"Рассылка завершена. Сообщение отправлено {success_count} пользователям.")
        await state.finish()
        await self.show_admin_panel(message)

    # ----- Конец обработчиков для Админ-Панели -----

async def main():
    # Асинхронная функция main
    bot = EnhancedBot()
    try:
        await bot.start()
    except (KeyboardInterrupt, SystemExit):
        logger.info("Бот остановлен")
    except Exception as e:
        logger.error(f"Критическая ошибка: {str(e)}", exc_info=True)

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

Assistant: Анализируя файл `bot.py`, можно выделить несколько ключевых аспектов его структуры, функциональности и потенциальных улучшений. Ниже представлен подробный разбор кода.

## Общая Структура и Архитектура

Файл `bot.py` реализует Telegram-бота с использованием библиотеки [`aiogram`](https://docs.aiogram.dev/en/latest/), которая является асинхронной и поддерживает современные возможности Telegram API. Бот управляет подписками пользователей, взаимодействует с различными API для обработки сообщений, генерации изображений и аудио, а также предоставляет административную панель для управления тарифами и пользователями.

### Основные Компоненты:

1. **Импорты:**
   - Стандартные библиотеки: `logging`, `asyncio`, `os`, `sqlite3`, `datetime`, `typing`.
   - Библиотеки для работы с Telegram: `aiogram`, `aiogram.contrib.fsm_storage.memory`, `aiogram.dispatcher`, `aiogram.dispatcher.filters.state`.
   - Внешние модули: `config`, различные API-клиенты (`sparrow_api`, `gpt4o_api`, `forgetapi`, `image_api`), обработчики аудио (`audio_processing`), функции из `doc.py`, а также `xlsxwriter` для генерации отчетов.

2. **Настройка Логирования:**
   ```python
   logging.basicConfig(level=logging.INFO)
   logger = logging.getLogger(__name__)
   ```
   Логирование настроено на уровень `INFO`, что позволяет отслеживать основные события и ошибки.

3. **Определение Состояний (FSM):**
   Класс `Form`, наследующий от `StatesGroup`, определяет различные состояния, через которые может проходить пользователь при взаимодействии с ботом, включая генерацию изображений, административные действия и генерацию отчетов.

4. **Класс `EnhancedBot`:**
   Основной класс, инкапсулирующий всю функциональность бота:
   - **Константы:** Например, `MAX_MESSAGE_LENGTH` ограничивает длину сообщений.
   - **Доступные Модели:** Список доступных AI-моделей и соответствующих им API.
   - **Инициализация:** Настройка бота, диспетчера, подключение к базе данных SQLite, инициализация API-клиентов, установка обработчиков сообщений.

5. **Методы для Управления Базой Данных:**
   - `_setup_database`: Создание необходимых таблиц (`users`, `tariff_plans`, `premium_chats`) и инициализация тарифных планов и премиум-чатов.
   - Методы для получения и установки информации о тарифах и пользователей, таких как `get_tariff_info`, `set_tariff_info`, `is_user_subscribed`, `set_user_subscription` и т.д.

6. **Обработчики Сообщений:**
   Методы для обработки различных типов сообщений и команд:
   - `cmd_start`: Обработка команды `/start`, проверка подписки пользователя и показ главного меню.
   - `show_model_selection`, `process_model_selection`: Управление выбором AI-модели.
   - `handle_clear_context`: Очистка контекста беседы.
   - `show_profile`: Отображение профиля пользователя.
   - `start_image_generation`, `process_image_prompt`, `process_image_size`: Генерация изображений по описанию пользователя.
   - `handle_document`, `_process_document`: Анализ и обработка присланных документов.
   - `handle_audio`, `_process_audio`: Обработка аудио и голосовых сообщений.
   - `handle_photo`, `_process_photo`: Обработка присланных фотографий.
   - `handle_text`, `_process_text_message`, `process_message`: Обработка текстовых сообщений с использованием выбранной AI-модели.
   - Административные обработчики (`show_admin_panel`, `handle_admin_selection` и т.д.) для управления тарифами, токенами, чатами и генерации отчетов.

7. **Методы Для Административной Панели:**
   Позволяют администраторам:
   - Изменять тарифы и их параметры.
   - Отправлять массовые рассылки пользователям.
   - Управлять списком премиум-чатов.
   - Генерировать отчеты о пользователях.

8. **Методы Взаимодействия с API:**
   Методы для взаимодействия с различными API (SparrowAPI, GPT4OAPI, ForgetAPI, ImageAPI) для обработки сообщений, генерации изображений и других операций.

9. **Главная Асинхронная Функция `main`:**
   Запускает бота и обрабатывает исключения при его работе.

## Детальный Разбор Основных Компонентов

### 1. Инициализация и Настройка Базы Данных

В методе `_setup_database` создаются необходимые таблицы, если они не существуют. Также инициализируются тарифные планы и премиум-чаты, если они отсутствуют в базе.

**Возможные Улучшения:**
- **Использование Асинхронной БД:** SQLite может стать узким местом при одновременной работе множества пользователей. Рассмотрите использование асинхронных баз данных, таких как `Databases` с PostgreSQL.
- **Миграции БД:** Для управления изменениями в структуре БД можно использовать инструменты миграции, такие как `Alembic`.

### 2. Управление Подписками и Тарифами

Бот проверяет подписку пользователя на определенный канал Telegram и обновляет его тарифный план в зависимости от принадлежности к премиум-чатам или статусу администратора.

**Потенциальные Проблемы:**
- **Обработка Ошибок API Telegram:** Если API Telegram недоступен или возникают временные ошибки, бот может неправильно определить статус подписки.
- **Безопасность:** Убедитесь, что доступ к базе данных защищен, особенно если бот размещен на публичных серверах.

### 3. Обработка Сообщений с Использованием FSM

Бот использует машину состояний (FSM) для управления диалогами, такими как генерация изображений или административные действия. Это позволяет поддерживать интерфейс, понятный пользователю, и управлять сложными сценариями взаимодействия.

**Примечания:**
- **Очистка Контекста:** При смене модели или очистке контекста беседы удаляются соответствующие данные, что позволяет пользователю начать новый диалог без накопившейся истории.
- **Параллельная Обработка Задач:** Использование `asyncio.create_task` для обработки долгих операций гарантирует, что бот не будет блокирован.

### 4. Взаимодействие с Различными API

Бот поддерживает взаимодействие с несколькими API для различных задач:
- **SparrowAPI и GPT4OAPI:** Для обработки текстовых сообщений и ведения беседы.
- **ForgetAPI:** Возможно, для обработки конкретных задач, связанных с забыванием или удалением данных.
- **ImageAPI:** Для генерации изображений по описанию пользователя.

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

### 5. Административная Панель

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

**Потенциальные Проблемы:**
- **Безопасность:** Убедитесь, что доступ к административным функциям ограничен только доверенным пользователям (`config.ADMIN_IDS`).
- **Валидация Входных Данных:** При изменении тарифов и токенов важно тщательно проверять вводимые данные, чтобы избежать некорректных записей в базе данных.

### 6. Генерация Отчетов

Используется библиотека `xlsxwriter` для создания отчетов в формате Excel, содержащих информацию о пользователях, их тарифах и использовании токенов.

**Возможные Улучшения:**
- **Автоматизация Файловых Операций:** Убедитесь, что временные файлы правильно удаляются после отправки, чтобы избежать накопления ненужных данных.
- **Асинхронность:** В текущей реализации работа с файлами выполняется синхронно. Рассмотрите возможность использования асинхронных операций для улучшения производительности.

## Рекомендации по Улучшению Кода

1. **Разделение Логики на Модули:**
   - Текущий файл `bot.py` довольно большой и содержит множество различных функций. Разделение логики на отдельные модули (например, `handlers`, `services`, `utils`) улучшит читаемость и поддерживаемость кода.

2. **Использование Асинхронных ORM:**
   - Вместо прямого использования `sqlite3` рассмотрите возможность использования асинхронных ORM, таких как [`Tortoise ORM`](https://tortoise-orm.readthedocs.io/en/latest/) или [`SQLAlchemy` с `asyncio`](https://docs.sqlalchemy.org/en/14/orm/extensions/asyncio.html), что повысит производительность и удобство работы с базой данных.

3. **Логирование:**
   - Улучшите логирование, добавив более подробные сообщения и, возможно, отдельные лог-файлы для различных типов событий (например, ошибок, информационных сообщений).

4. **Обработка Исключений:**
   - Добавьте более детальную обработку исключений, особенно при взаимодействии с внешними API и базой данных, чтобы избежать падения бота при возникновении непредвиденных ситуаций.

5. **Конфигурация через Переменные Окружения:**
   - Убедитесь, что все конфиденциальные данные (токены, ключи API) хранятся безопасно, например, через переменные окружения, а не в файлах конфигурации.

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

7. **Документация:**
   - Добавьте подробные комментарии и документацию к методам и классам, чтобы облегчить понимание кода для других разработчиков и будущих изменений.

8. **Оптимизация Работы с Токенами:**
   - Возможно, стоит внедрить более точный подсчет использованных токенов, особенно если различные API имеют разные модели подсчета.

## Заключение

Файл `bot.py` представляет собой функционально богатого Telegram-бота с поддержкой множества функций, включая обработку различных типов сообщений, управление подписками и тарифными планами, а также административные возможности. Несмотря на обширную функциональность, существует пространство для улучшений, особенно в плане организации кода, обработки ошибок и производительности. Внедрение предложенных рекомендаций поможет сделать бота более устойчивым, масштабируемым и удобным в поддержке.
User: вот такая ошибка при переводе текста с gpt-4o-mini
ERROR:__main__:Ошибка при переводе описания: ForgetAPI.send_message() got an unexpected keyword argument 'message'
Traceback (most recent call last):
  File "/root/bigbot/bot.py", line 837, in translate_prompt 
    response = await asyncio.to_thread(
  File "/usr/lib/python3.10/asyncio/threads.py", line 25, in to_thread
    return await loop.run_in_executor(None, func_call)
  File "/usr/lib/python3.10/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
TypeError: ForgetAPI.send_message() got an unexpected keyword argument 'message' переводе описания: ForgetAPI.send_message() got an unexpected keyword argument 'message'
Traceback (most recent call last):
  File "/root/bigbot/bot.py", line 837, in translate_prompt 
    response = await asyncio.to_thread(
  File "/usr/lib/python3.10/asyncio/threads.py", line 25, in to_thread
    return await loop.run_in_executor(None, func_call)
  File "/usr/lib/python3.10/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
TypeError: ForgetAPI.send_message() got an unexpected keyword argument 'message'

также нужно добавить к gpt-4o поиск в интернете по ключевым словам типа [найди, поиск, найти в интернете, погода добавь сам еще] чтобы там например я писал погода в москве он писал погоду в москве на основе поиска или найди последнюю версию aiogram в интернете 
вот и напиши весь код полностью без сокращений, проверь структуру как я отправляю сообщения gpt-4o и сделай все правильно, я тебя прошу, перепроверяй 
а еще когда другой пользователь пишет, например фри ему почему-то пишет что ошибка в настройке api, это тоже надо исправить, у админа например ии отвечает, а фри нет