привет! вот код телеграм бота. изучи его и просто жди пока я не напишу следующее сообщение
import logging
import requests
import json
import os
import re
import html
import sys
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from telegram import (
    Update,
    InlineKeyboardMarkup,
    InlineKeyboardButton,
    InputFile
)
from telegram.constants import ParseMode
from telegram.ext import (
    ApplicationBuilder,
    CommandHandler,
    MessageHandler,
    ContextTypes,
    filters,
    CallbackQueryHandler,
    Defaults,
)
from telegram.error import TelegramError
import openai
import asyncio
import aiohttp 
from PIL import Image
import io

# Функция для загрузки конфигурации
def load_config():
    script_dir = os.path.dirname(os.path.abspath(__file__))
    config_path = os.path.join(script_dir, 'config.json')

    if not os.path.exists(config_path):
        raise FileNotFoundError(f"файл config.json не найден по пути: {config_path}")

    with open(config_path, 'r', encoding='utf-8') as f:
        return json.load(f)

# Функция для сохранения контекста чата в файл
def save_chat_context(user_id, model_id, chat_slot, context):
    chats_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'chats')
    if not os.path.exists(chats_dir):
        os.makedirs(chats_dir)

    filename = f"{user_id}_{model_id}_chat{chat_slot}.json"
    file_path = os.path.join(chats_dir, filename)

    try:
        with open(file_path, 'w', encoding='utf-8') as f:
            json.dump(context, f, ensure_ascii=False, indent=4)
    except Exception as e:
        logger.error(f"ошибка при сохранении контекста чата: {e}")

# Функция для загрузки контекста чата из файла
def load_chat_context(user_id, model_id, chat_slot, system_instruction):
    chats_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'chats')
    filename = f"{user_id}_{model_id}_chat{chat_slot}.json"
    file_path = os.path.join(chats_dir, filename)

    if not os.path.exists(file_path):
        return [{"role": "system", "content": system_instruction}]

    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            return json.load(f)
    except Exception as e:
        logger.error(f"ошибка при загрузке контекста чата: {e}")
        return [{"role": "system", "content": system_instruction}]

# Функция для загрузки и сохранения использования
def load_usage():
    usage_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'usage.json')
    if not os.path.exists(usage_path):
        return {}
    try:
        with open(usage_path, 'r', encoding='utf-8') as f:
            return json.load(f)
    except Exception as e:
        logger.error(f"ошибка при загрузке usage.json: {e}")
        return {}

def save_usage():
    usage_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'usage.json')
    try:
        with open(usage_path, 'w', encoding='utf-8') as f:
            json.dump(usage_store, f, ensure_ascii=False, indent=4)
    except Exception as e:
        logger.error(f"ошибка при сохранении usage.json: {e}")

# Функция для проверки и инкремента лимитов
def check_and_increment_limit(user_id, model_id):
    user_limits = config.get('user_limits', {})
    user_id_str = str(user_id)
    if user_id_str not in user_limits:
        return True  # Нет лимитов для пользователя

    model_limits = user_limits[user_id_str]
    if model_id not in model_limits:
        return True  # Нет лимитов для данной модели

    limit = model_limits[model_id]
    if limit == "infinite":
        return True

    # Получаем текущее использование
    user_usage = usage_store.get(user_id_str, {})
    model_usage = user_usage.get(model_id, 0)

    if model_usage >= limit:
        return False

    # Увеличиваем использование
    usage_store.setdefault(user_id_str, {})[model_id] = model_usage + 1
    save_usage()
    return True

# Загрузка конфигурации
try:
    config = load_config()
except FileNotFoundError as e:
    print(e)
    exit(1)
except json.JSONDecodeError as e:
    print(f"Ошибка разбора config.json: {e}")
    exit(1)

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

# Настройка OpenAI API
openai_api_key = config.get('openai_api_key')
if not openai_api_key:
    logger.error("OpenAI API ключ не указан в config.json.")
    exit(1)
openai.api_key = openai_api_key

# Ваши API-эндпоинты (cablyai)
API_URL_TEXT = 'https://cablyai.com/v1/chat/completions'
API_URL_IMAGE = 'https://cablyai.com/v1/images/generations' # URL для генерации изображений
API_HEADERS = {
    'Authorization': f"Bearer {config['api_key']}",
    'Content-Type': 'application/json'
}

# Инструкция для текстовых моделей
SYSTEM_INSTRUCTION_TEXT = config.get('system_instruction', "ты ведёшь себя как котик")

# Структуры для хранения контекста и активных чатов
context_store = {}
user_state = {}

# Загрузка использования
usage_store = load_usage()

MAX_MESSAGE_LENGTH = 4096

# Преобразуем список авторизованных пользователей в множество для быстрого поиска
AUTHORIZED_USER_IDS = set(map(int, config.get('authorized_user_ids', [])))

# Хранение режима пользователя ('message' или 'file')
user_mode_store = {}
user_modes_file = 'user_modes.json'

# Функция для загрузки режимов пользователей из файла
def load_user_modes():
    if os.path.exists(user_modes_file):
        try:
            with open(user_modes_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        except Exception as e:
            logger.error(f"Ошибка при загрузке режимов пользователей: {e}")
            return {}
    else:
        return {}

# Функция для сохранения режимов пользователей в файл
def save_user_modes():
    try:
        with open(user_modes_file, 'w', encoding='utf-8') as f:
            json.dump(user_mode_store, f, ensure_ascii=False, indent=4)
    except Exception as e:
        logger.error(f"Ошибка при сохранении режимов пользователей: {e}")

# Загрузка режимов пользователей при запуске бота
user_mode_store = load_user_modes()

# Создание маппинга model_id -> model_info и model_id -> model_name для быстрого доступа
model_id_to_info = {}
model_id_to_name = {}
for model_name, model_info in config.get('models', {}).items():
    model_id = model_info.get('id')
    if model_id:
        model_id_to_info[model_id] = model_info
        model_id_to_name[model_id] = model_name.lower()
    else:
        logger.warning(f"Модель '{model_name}' не имеет 'id'.")

# Список ограниченных моделей
RESTRICTED_MODELS = ['o1-preview', 'o1-mini', 'openai-o1-preview', 'openai-o1-mini']
AUTHORIZED_MODEL_USERS = {5005816138, 1759650343, 7856892361, 488179251, 1110920711}

# Описания моделей
MODEL_DESCRIPTIONS = {
    "claude-3.5-sonnet-20241022": "передовая модель Anthropic. быстрая, в задачах с кодом в чём-то лучше даже OpenAI o1-mini и preview. актуальность знаний: до апреля 2024. контекстное окно: 200000 токенов. выплюнуть за раз может: 5000 символов ( символ не равно токен ).",
    "claude-3-opus": "бывшая передовая модель Anthropic. медленная, но очень хороша в генерации текстов и удивляет своими познаниями русского языка. актуальность знаний: до августа 2023. контекстное окно: 200000 токенов. выплюнуть за раз может: 4096 токенов.",
    "claude-3.5-haiku": "почти передовая модель Anthropic. совсем новая, быстрая, лучше чем предшественник Claude 3 Haiku. её учили хорошо писать код! актуальность знаний: неизвестна. контекстное окно: 200000 токенов. выплюнуть за раз может: 4096 токенов.",
    "chatgpt-4o-latest": "новейшая версия модели GPT-4o за, вроде, сентябрь 2024, созданная OpenAI. быстрая, хороша для широкого спектра задач. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 16384 токена.",
    "gpt-4-turbo": "бывшая передовая модель OpenAI. иногда в чём-то превосходит даже GPT-4o-latest, но уступает в скорости и сильно дороже. актуальность знаний: до декабря 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 4096 токенов.",
    "gpt-4o-mini": "уменьшенная версия GPT-4o. очень быстрая! но по качеству ответов уступает. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 16384 токена.",
    "o1-preview": "передовая модель OpenAI. использует цепочку рассуждений для ответов, что повышает их точность и размер, но генерирует ответ довольно долго относительно других моделей. в точных науках и коде показывает себя лучше любых моделей. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 65536 токенов.",
    "o1-mini": "уменьшенная версия модели o1 от OpenAI. тоже использует цепочку рассуждений, но думает намного быстрее. может уступать o1-preview в качестве ответов, но это компенсируется её на 80% более низкой ценой (для тех кто платит за неё) и быстротой ответов. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 32768 токенов.",
    "llama-3.1-405b-turbo": "текстовая модель. сопоставима по качеству ответов с остальными моделями, но может уступать, и выдавать всякие иероглифы посреди ответа. но почему бы и не добавить? актуальность знаний: до декабря 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: неизвестно. ",
    "FLUX.1.1 [pro]": "лучшая существующая модель генерации изображений. некоторые её изображения неотличимы от реальных! но она нестабильна. но ты всегда можешь отправить новый запрос! контекста не имеет, работая лишь с текущим заданным запросом.",
    "flux.1.1-pro-ultra": "новая версия лучшей существующей модели генерации изображений! некоторые её изображения неотличимы от реальных! но она нестабильна. но ты всегда можешь отправить новый запрос! контекста не имеет, работая лишь с текущим заданным запросом.",
    "sd3.5": "модель для генерации изображений по текстовому описанию. контекста не имеет, работая лишь с текущим заданным запросом.",
    "stable-diffusion-3.5-turbo": "более новая и быстрая модель для генерации изображений по текстовому описанию. контекста не имеет, работая лишь с текущим заданным запросом.",
    "openai-chatgpt-4o-latest": "новейшая версия модели GPT-4o за, вроде, сентябрь 2024, созданная OpenAI. быстрая, хороша для широкого спектра задач. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 16384 токена. отличия от подобной модели выше: поставляется с помощью ключа api OpenAI.",
    "openai-gpt-4-turbo": "бывшая передовая модель OpenAI. иногда в чём-то превосходит даже GPT-4o-latest, но уступает в скорости. актуальность знаний: до декабря 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 4096 токенов. отличия от подобной модели выше: поставляется с помощью ключа api OpenAI.",
    "openai-gpt-4o-mini": "уменьшенная версия GPT-4o. очень быстрая! но по качеству ответов уступает. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 16384 токена. отличия от подобной модели выше: поставляется с помощью ключа api OpenAI.",
    "openai-o1-preview": "передовая модель OpenAI. использует цепочку рассуждений для ответов, что повышает их точность и размер, но генерирует ответ довольно долго относительно других моделей. в точных науках и коде показывает себя лучше любых моделей. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 32768 токенов. отличия от подобной модели выше: поставляется с помощью ключа api OpenAI.",
    "openai-o1-mini": "уменьшенная версия модели o1 от OpenAI. тоже использует цепочку рассуждений, но думает намного быстрее. может уступать o1-preview в качестве ответов, но это компенсируется её на 80% более низкой ценой (для тех кто платит за неё) и быстротой ответов. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 65536 токенов. отличия от подобной модели выше: поставляется с помощью ключа api OpenAI."
}

# Функция для разбивки длинного сообщения на части
def split_message(message, max_length):
    return [message[i:i + max_length] for i in range(0, len(message), max_length)]

# Функция для создания кнопок выбора модели
def get_model_buttons(user_id):
    buttons = []
    for name, model_info in config.get('models', {}).items():
        model_id = model_info['id']
        if model_id in RESTRICTED_MODELS and user_id not in AUTHORIZED_MODEL_USERS:
            buttons.append([InlineKeyboardButton(html.escape(name.lower()), callback_data=f"restricted:{model_id}")])
        else:
            buttons.append([InlineKeyboardButton(html.escape(name.lower()), callback_data=f"model_select:{model_id}")])
    return InlineKeyboardMarkup(buttons)

# Функция для создания кнопок выбора слота чата
def get_chat_slot_buttons(model_id):
    keyboard = [
        [
            InlineKeyboardButton("чат 1", callback_data=f"chat_slot:1:{model_id}"),
            InlineKeyboardButton("чат 2", callback_data=f"chat_slot:2:{model_id}")
        ]
    ]
    return InlineKeyboardMarkup(keyboard)

# Функция для создания основных кнопок: 'сменить модель', 'выбрать чат', 'очистить контекст', 'продолжить'
def get_main_buttons(user_id, model_id=None):
    buttons = []
    if model_id:
        buttons.append([
            InlineKeyboardButton("сменить модель", callback_data='change_model'),
            InlineKeyboardButton("выбрать чат", callback_data='change_chat_slot'),
            InlineKeyboardButton("очистить контекст", callback_data='clear_context')
        ])
        # Добавляем кнопку "Продолжить" на отдельный ряд
        buttons.append([
            InlineKeyboardButton("продолжить", callback_data='continue')
        ])
    else:
        buttons.append([
            InlineKeyboardButton("сменить модель", callback_data='change_model')
        ])
    return InlineKeyboardMarkup(buttons)

# Функция для преобразования Markdown в HTML
def markdown_to_html(text):
    """
    Простой конвертер Markdown в HTML.
    Обрабатывает основные элементы Markdown.
    """
    # Экранирование HTML-символов
    text = html.escape(text)

    # Замена **жирный** на <b>жирный</b>
    text = re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', text)

    # Замена *курсив* на <i>курсив</i>
    text = re.sub(r'\*(.*?)\*', r'<i>\1</i>', text)

    # Замена __подчёркивание__ на <u>подчёркивание</u>
    text = re.sub(r'__(.*?)__', r'<u>\1</u>', text)

    # Замена ~~зачёркивание~~ на 
зачёркивание

    text = re.sub(r'~~(.*?)~~', r'
\1
', text)

    # Обработка заголовков
    text = re.sub(r'###### (.*?)\n', r'<b>\1</b>\n', text)
    text = re.sub(r'##### (.*?)\n', r'<b>\1</b>\n', text)
    text = re.sub(r'#### (.*?)\n', r'<b>\1</b>\n', text)
    text = re.sub(r'### (.*?)\n', r'<b>\1</b>\n', text)
    text = re.sub(r'## (.*?)\n', r'<b>\1</b>\n', text)
    text = re.sub(r'# (.*?)\n', r'<b>\1</b>\n', text)

    return text

# Функция для сохранения ответа в .md файл
def save_md_file(content, user_id):
    """
    Сохраняет содержимое в .md файл с уникальным именем.
    Возвращает путь к файлу.
    """
    if not os.path.exists('responses'):
        os.makedirs('responses')

    timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
    filename = f"responses/{user_id}_{timestamp}.md"

    try:
        with open(filename, 'w', encoding='utf-8') as f:
            f.write(content)
        return filename
    except Exception as e:
        logger.error(f"Ошибка при сохранении файла .md: {e}")
        return None

# Функция для проверки, поддерживает ли модель обработку изображений
def is_image_supported(model_id):
    supported_models = [
        'openai-chatgpt-4o-latest',
        'openai-gpt-4-turbo',
        'openai-gpt-4o-mini',
        'chatgpt-4o-latest',
        'gpt-4-turbo',
        'gpt-4o-mini',
        "FLUX.1.1 [pro]",
        "sd3.5",
        "flux.1.1-pro-ultra",
        "stable-diffusion-3.5-turbo"
    ]
    return model_id in supported_models

# Функция для получения корректного идентификатора модели для API
def get_api_model_id(model_id, provider):
    if provider == 'openai' and model_id.startswith('openai-'):
        return model_id[len('openai-'):]
    return model_id

# Функция для отправки запроса через cablyai API
async def send_cablyai_request(model_id, messages=None, prompt=None, model_type='text'):
    try:
        if model_type in ['text', 'multimodal']:
            data = {
                "model": model_id,
                "messages": messages
            }
            url = API_URL_TEXT
        elif model_type == 'image':
            data = {
                "model": model_id,
                "prompt": prompt,
                "n": 1,
                "size": "1024x1024"  # Можно адаптировать под нужный размер
            }
            url = API_URL_IMAGE
        else:
            logger.error(f"Неизвестный тип модели: {model_type}")
            return None

        async with aiohttp.ClientSession() as session:
            async with session.post(url, headers=API_HEADERS, json=data) as response:
                if response.status == 200:
                    return await response.json()
                else:
                    error_text = await response.text()
                    logger.error(f"Ошибка запроса к CablyAI API: {response.status} - {error_text}")
                    return None
    except Exception as e:
        logger.error(f"Ошибка при запросе к CablyAI API: {e}")
        return None

# Асинхронная функция для перевода текста с русского на английский с помощью модели CablyAI
async def translate_to_english(original_text):
    translation_prompt = f"переведи следующий текст на английский язык и верни только переведенный текст:\n\n{original_text}"
    
    translation_data = {
        "model": "chatgpt-4o-latest",
        "messages": [
            {"role": "system", "content": "you are a helpful assistant that translates russian text to english."},
            {"role": "user", "content": translation_prompt}
        ]
    }
    
    try:
        response = await send_cablyai_request("chatgpt-4o-latest", messages=translation_data['messages'], model_type='multimodal')
        if response and "choices" in response and len(response["choices"]) > 0:
            translated_text = response["choices"][0].get('message', {}).get('content', '').strip()
            return translated_text
        else:
            logger.error("неверная структура ответа от модели при переводе.")
            return None
    except Exception as e:
        logger.error(f"ошибка при переводе текста: {e}")
        return None

# Функция для генерации изображений через CablyAI
async def generate_image_cablyai(model_id, prompt):
    """
    Генерирует изображение через CablyAI на основе предоставленного промпта.
    Возвращает URL сгенерированного изображения или None в случае ошибки.
    """
    response_data = await send_cablyai_request(model_id, prompt=prompt, model_type='image')
    if response_data and 'data' in response_data and len(response_data['data']) > 0:
        image_url = response_data['data'][0].get('url')
        return image_url
    else:
        logger.error("Не удалось сгенерировать изображение через CablyAI.")
        return None

# Функция для обработки изображений через соответствующий API
async def process_image_request(user_id, model_id, prompt, image_path=None):
    model_info = model_id_to_info.get(model_id, {})
    provider = model_info.get('provider', 'cablyai')
    
    if provider == 'cablyai':
        # Генерация изображения через CablyAI
        image_url = await generate_image_cablyai(model_id, prompt)
        return image_url
    elif provider == 'openai':
        # Обработка изображения через OpenAI
        # Пример, требуется адаптация под актуальный OpenAI API
        try:
            if image_path:
                # Конвертация изображения в PNG и проверка размера
                with Image.open(image_path) as img:
                    img = img.convert("RGBA")
                    with io.BytesIO() as output:
                        img.save(output, format="PNG")
                        png_data = output.getvalue()
                
                if len(png_data) > 4 * 1024 * 1024:
                    # Если размер превышает 4MB, уменьшить качество или размер
                    img.thumbnail((1024, 1024))
                    with io.BytesIO() as output:
                        img.save(output, format="PNG", optimize=True)
                        png_data = output.getvalue()
                    if len(png_data) > 4 * 1024 * 1024:
                        logger.error("Размер изображения после конвертации превышает 4MB.")
                        return None

                # Сохранение конвертированного изображения
                with open(image_path, 'wb') as f:
                    f.write(png_data)
                
                # Пример использования OpenAI API для обработки изображения
                with open(image_path, 'rb') as image_file:
                    response = openai.Image.create_edit(
                        image=image_file,
                        prompt=prompt,
                        n=1,
                        size="1024x1024"
                    )
                # Извлечение URL сгенерированного изображения
                image_url = response['data'][0]['url']
                return image_url
            else:
                # Если изображение не предоставлено, обрабатываем только текст
                response = openai.ChatCompletion.create(
                    model=get_api_model_id(model_id, provider),
                    messages=[
                        {"role": "system", "content": "You are a helpful assistant."},
                        {"role": "user", "content": prompt}
                    ]
                )
                answer = response['choices'][0]['message']['content']
                return answer
        except openai.error.OpenAIError as e:
            logger.error(f"Ошибка OpenAI API при обработке изображения: {e}")
            return None
    elif provider == 'anthropic':
        # Пример обработки изображений для Anthropic моделей (запросите документацию провайдера)
        logger.warning(f"Обработка изображений для провайдера Anthropic модели {model_id} не реализована.")
        return "Обработка изображений для этой модели не поддерживается."
    else:
        logger.error(f"Неизвестный провайдер модели: {provider}")
        return None

# Функция для отправки запроса через OpenAI API
async def send_openai_request(model_id, messages=None, prompt=None, image_path=None):
    api_model_id = get_api_model_id(model_id, 'openai')
    try:
        if 'gpt-4' in api_model_id or 'chatgpt' in api_model_id or 'o1' in api_model_id:
            if image_path:
                # Обработка изображения через OpenAI
                try:
                    with open(image_path, 'rb') as image_file:
                        response = openai.Image.create_edit(
                            image=image_file,
                            prompt=messages[-1]['content'],  # Последнее сообщение пользователя как промпт
                            n=1,
                            size="1024x1024"
                        )
                    # Извлечение URL сгенерированного изображения
                    image_url = response['data'][0]['url']
                    return image_url
                except openai.error.OpenAIError as e:
                    logger.error(f"Ошибка OpenAI API при обработке изображения: {e}")
                    return None
            else:
                response = openai.ChatCompletion.create(
                    model=api_model_id,
                    messages=messages
                )
                return response
        else:
            # Обработка других моделей, если необходимо
            logger.error(f"Модель {model_id} не поддерживается OpenAI API.")
            return None
    except openai.error.OpenAIError as e:
        logger.error(f"Ошибка OpenAI API: {e}")
        return None

# Обработчик команды /setmode
async def setmode(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Позволяет пользователю выбрать режим получения ответов:
    - message: получение текстовых сообщений
    - file: получение ответов в .md файлах
    """
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался использовать /setmode: {user_id}")
        await update.message.reply_text('тебе сюда мяукать нельзя.')
        return

    if len(context.args) != 1:
        await update.message.reply_text('использование: /setmode [message|file]')
        return

    mode = context.args[0].lower()
    if mode not in ['message', 'file']:
        await update.message.reply_text('неверный режим. к команде надо дописать "message" или "file".')
        return

    user_mode_store[str(user_id)] = mode  # Сохраняем как строку для сериализации в JSON
    save_user_modes()
    await update.message.reply_text(f'Режим получения ответов установлен на: <b>{mode}</b>', parse_mode=ParseMode.HTML)

# Обработчик команды /mode
async def mode(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Позволяет пользователю узнать текущий режим получения ответов.
    """
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался использовать /mode: {user_id}")
        await update.message.reply_text('тебе сюда мяукать нельзя.')
        return

    mode = user_mode_store.get(str(user_id), 'message')  # По умолчанию 'message'
    await update.message.reply_text(f'текущий режим получения ответов: <b>{mode}</b>', parse_mode=ParseMode.HTML)

# Обработчик команды /instructions
async def instructions(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Отправляет пользователю инструкции по использованию бота.
    """
    menu_text = (
        "<b>инструкция по использованию бота:</b>\n\n"
        "<b>/start</b> - запустить бота и выбрать модель для взаимодействия.\n"
        "<b>/setmode [message|file]</b> - выбрать режим получения ответов (message - текстовые сообщения, file - .md файлы, они нужны для того чтобы получить полный ответ модели без ограничения длины сообщения телеграма).\n"
        "<b>/mode</b> - узнать текущий выбранный режим.\n"
        "<b>/instructions</b> - получить инструкции по использованию бота.\n"
        "<b>/models</b> - получить описание доступных моделей.\n"
        "<b>/limits</b> - посмотреть ваши текущие лимиты и оставшиеся запросы на сегодня.\n\n"
        "также доступно использование кнопок ниже:\n"
        "<b>сменить модель</b> - выбрать другую модель для общения.\n"
        "<b>продолжить</b> - продолжить ответ модели.\n"
        "‼️<b>важно:</b> токен не равно символ. все модели воспринимают токен как единицу измерения по разному, будь то слово, кусок слова или символ. если про модель указано что она отвечает определённым количеством символов - так и есть.\n"
        "‼️<b>лимиты бота:</b> если у тебя есть доступ к этому боту - лимитов никаких нет кроме как на o1 модели (если тебе к ним открыт доступ). админ бота не гарантирует, что бот сможет работать бесконечно, всё зависит от провайдера API ключей.\n"
        "‼️<b>контекст:</b> в боте доступны чаты - 2 на каждую модель. ты можешь очистить контекст определённого чата, если надо."
    )

    await update.message.reply_text(menu_text, parse_mode=ParseMode.HTML)

# Обработчик команды /models
async def models_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Отправляет пользователю список доступных моделей с описаниями.
    Разбивает вывод на части по 10 моделей для соответствия лимитам Telegram.
    """
    user_id = update.effective_user.id
    models = config.get('models', {})
    if not models:
        await update.message.reply_text('Нет доступных моделей.', reply_markup=get_main_buttons(user_id=user_id))
        return

    descriptions = []
    for name, info in models.items():
        model_id = info.get('id')
        model_name = name.lower()
        model_type = info.get('type', 'неизвестный тип').lower()
        description = MODEL_DESCRIPTIONS.get(model_id, "Описание недоступно.")
        descriptions.append(f"- <b>{html.escape(model_name)}</b>: {html.escape(description)}")

    # Разбить на чанки по 10 моделей
    chunk_size = 10
    for i in range(0, len(descriptions), chunk_size):
        chunk = descriptions[i:i+chunk_size]
        message = "<b>Описание доступных моделей:</b>\n\n" + "\n".join(chunk)
        await update.message.reply_text(message, parse_mode=ParseMode.HTML)

# Обработчик команды /start
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался использовать /start: {user_id}")
        await update.message.reply_text('Тебе сюда мяукать нельзя.')
        return

    # Инициализация структуры данных для пользователя
    if user_id not in context_store:
        context_store[user_id] = {}
    if user_id not in user_state:
        user_state[user_id] = {'current_model_id': None, 'current_chat_slot': None}

    # Кнопки для выбора модели
    if config.get('models'):
        await update.message.reply_text(
            'Привет! Выбери модель для общения:',
            reply_markup=get_model_buttons(user_id)
        )
    else:
        await update.message.reply_text(
            'Привет!\n\nНет доступных моделей для выбора.',
            reply_markup=get_main_buttons(user_id)
        )

# Обработчик команды /reload
async def reload_bot(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Перезагружает бота, обновляя конфигурации.
    """
    user_id = update.effective_user.id
    admin_ids = {5005816138} # ID администраторов

    if user_id not in admin_ids:
        await update.message.reply_text('Ты не админ и тебе такое нельзя.')
        return

    await update.message.reply_text('Перезагружаю конфигурацию бота...')
    logger.info("Перезагрузка конфигурации бота по команде /reload")

    try:
        global config, model_id_to_info, model_id_to_name, AUTHORIZED_USER_IDS, RESTRICTED_MODELS, AUTHORIZED_MODEL_USERS, SYSTEM_INSTRUCTION_TEXT, user_limits
        config = load_config()

        # Обновление ключевых переменных
        AUTHORIZED_USER_IDS = set(map(int, config.get('authorized_user_ids', [])))

        model_id_to_info.clear()
        model_id_to_name.clear()
        for model_name, model_info in config.get('models', {}).items():
            model_id = model_info.get('id')
            if model_id:
                model_id_to_info[model_id] = model_info
                model_id_to_name[model_id] = model_name.lower()
            else:
                logger.warning(f"Модель '{model_name}' не имеет 'id'.")

        RESTRICTED_MODELS = ['o1-preview', 'o1-mini', 'openai-o1-preview', 'openai-o1-mini']
        AUTHORIZED_MODEL_USERS = {5005816138, 1759650343, 7856892361, 488179251}  # Обновите при необходимости

        SYSTEM_INSTRUCTION_TEXT = config.get('system_instruction', "ты ведёшь себя как котик")

        # Перезагрузка user_limits и usage_store
        usage_store.clear()
        usage_store.update(load_usage())

        # Загрузка режимов пользователей
        user_mode_store.clear()
        user_mode_store.update(load_user_modes())

        await update.message.reply_text('Конфигурация успешно перезагружена.')
        logger.info("Конфигурация бота успешно перезагружена.")
    except Exception as e:
        logger.error(f"Ошибка при перезагрузке конфигурации: {e}")
        await update.message.reply_text('Произошла ошибка при перезагрузке конфигурации.')

# Обработчик выбора модели через кнопки
async def select_model(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    query = update.callback_query
    await query.answer()

    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался выбрать модель: {user_id}")
        await query.edit_message_text('Тебе сюда мяукать нельзя.')
        return

    selected_model_data = query.data

    # Проверка, является ли модель ограниченной
    if selected_model_data.startswith("restricted:"):
        model_id = selected_model_data.split("restricted:")[1]
        if user_id not in AUTHORIZED_MODEL_USERS:
            logger.warning(f"Пользователь {user_id} попытался использовать ограниченную модель {model_id}")
            await query.edit_message_text('Вам нельзя использовать эту модель.')
            return
    elif selected_model_data.startswith("model_select:"):
        model_id = selected_model_data.split("model_select:")[1]
    else:
        logger.error(f"Неправильный формат данных callback: {selected_model_data}")
        await query.edit_message_text('Произошла ошибка при выборе модели.')
        return

    model_info = model_id_to_info.get(model_id)

    if not model_info:
        logger.error(f"Выбранная модель не найдена в конфигурации: {model_id}")
        await query.edit_message_text('Выбранная модель не найдена в конфигурации.')
        return

    # Проверка и инкремент лимита
    if not check_and_increment_limit(user_id, model_id):
        await query.edit_message_text('Вы достигли лимита запросов для этой модели.')
        return

    # Обновляем текущую модель пользователя
    user_state[user_id]['current_model_id'] = model_id

    # Предлагаем выбрать слот чата (1 или 2)
    await query.edit_message_text(
        f'Выбрана модель: <b>{html.escape(model_id_to_name.get(model_id, "Неизвестная модель"))}</b>\n\nВыбери слот чата:',
        parse_mode=ParseMode.HTML,
        reply_markup=get_chat_slot_buttons(model_id)
    )

# Обработчик выбора слота чата
async def select_chat_slot(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    query = update.callback_query
    await query.answer()

    data = query.data  # Формат: "chat_slot:{slot}:{model_id}"
    try:
        _, slot_str, model_id = data.split(":")
        chat_slot = int(slot_str)
    except ValueError:
        logger.error(f"Некорректные данные callback для выбора чата: {data}")
        await query.edit_message_text('Произошла ошибка при выборе чата.')
        return

    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался выбрать чат: {user_id}")
        await query.edit_message_text('Тебе сюда мяукать нельзя.')
        return

    # Проверяем и инкрементируем лимит после выбора чата
    if not check_and_increment_limit(user_id, model_id):
        await query.edit_message_text('Вы достигли лимита запросов для этой модели.')
        return

    # Обновляем текущий слот чата пользователя
    user_state[user_id]['current_chat_slot'] = chat_slot
    user_state[user_id]['current_model_id'] = model_id

    # Загружаем контекст чата
    model_info = model_id_to_info.get(model_id, {})
    provider = model_info.get('provider', 'cablyai')
    system_instruction = SYSTEM_INSTRUCTION_TEXT
    if provider == 'openai' and model_id not in ['openai-o1-preview', 'openai-o1-mini']:
        system_instruction = "You are a helpful assistant."

    context_store.setdefault(user_id, {}).setdefault(model_id, {})
    if chat_slot not in context_store[user_id][model_id]:
        context_store[user_id][model_id][chat_slot] = load_chat_context(user_id, model_id, chat_slot, system_instruction)
    else:
        # Если контекст уже загружен, ничего не делаем
        pass

    model_name = html.escape(model_id_to_name.get(model_id, "Неизвестная модель"))
    await query.edit_message_text(
        f'Выбран <b>Чат {chat_slot}</b> для модели <b>{model_name}</b>',
        parse_mode=ParseMode.HTML,
        reply_markup=get_main_buttons(user_id, model_id)
    )

# Обработчик кнопки "сменить модель"
async def change_model_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    query = update.callback_query
    await query.answer()

    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь пытался сменить модель: {user_id}")
        await query.edit_message_text('Тебе сюда мяукать нельзя.')
        return

    # Получаем текущую модель для логирования
    current_model_id = user_state.get(user_id, {}).get('current_model_id')
    logger.info(f"Пользователь {user_id} меняет модель. Текущая модель: {current_model_id}")

    # Обновляем состояние пользователя
    user_state[user_id]['current_model_id'] = None
    user_state[user_id]['current_chat_slot'] = None

    # Очищаем текущий контекст
    if current_model_id and current_model_id in context_store[user_id]:
        context_store[user_id][current_model_id] = {}

    # Отображаем кнопки выбора модели
    try:
        await query.edit_message_text('Выбери модель для взаимодействия:', reply_markup=get_model_buttons(user_id))
    except Exception as e:
        logger.error(f"Ошибка при отображении кнопок выбора модели: {e}")
        await query.edit_message_text('Произошла ошибка при смене модели. Попробуй позже.', reply_markup=get_main_buttons(user_id, current_model_id))

# Обработчик кнопки "выбрать чат"
async def change_chat_slot_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    query = update.callback_query
    await query.answer()

    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь пытался выбрать чат: {user_id}")
        await query.edit_message_text('тебе сюда мяукать нельзя.')
        return

    # Получаем текущую выбранную модель пользователя
    current_model_id = user_state.get(user_id, {}).get('current_model_id')
    if not current_model_id:
        await query.edit_message_text('модель не выбрана. используй /start для выбора модели.')
        return

    # Предлагаем выбрать слот чата для текущей модели
    await query.edit_message_text(
        f'Выбери слот для модели <b>{html.escape(model_id_to_name.get(current_model_id, "Неизвестная модель"))}</b>:',
        parse_mode=ParseMode.HTML,
        reply_markup=get_chat_slot_buttons(current_model_id)
    )

# Обработчик кнопки "очистить контекст"
async def clear_context_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    query = update.callback_query
    await query.answer()

    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался очистить контекст: {user_id}")
        await query.edit_message_text('тебе сюда мяукать нельзя.')
        return

    # Получаем текущую выбранную модель и чат пользователя
    current_model_id = user_state.get(user_id, {}).get('current_model_id')
    current_chat_slot = user_state.get(user_id, {}).get('current_chat_slot')
    if not current_model_id or not current_chat_slot:
        await query.edit_message_text('модель или чат не выбраны. используй /start для выбора модели и чата.')
        return

    # Проверка и инкремент лимита перед очисткой
    if not check_and_increment_limit(user_id, current_model_id):
        await query.edit_message_text('Вы достигли лимита запросов для этой модели.')
        return

    # Очищаем контекст
    model_info = model_id_to_info.get(current_model_id, {})
    provider = model_info.get('provider', 'cablyai')
    system_instruction = SYSTEM_INSTRUCTION_TEXT
    if provider == 'openai' and current_model_id not in ['openai-o1-preview', 'openai-o1-mini']:
        system_instruction = "You are a helpful assistant."

    context_store[user_id][current_model_id][current_chat_slot] = [{"role": "system", "content": system_instruction}]
    save_chat_context(user_id, current_model_id, current_chat_slot, context_store[user_id][current_model_id][current_chat_slot])

    await query.edit_message_text(
        f'Контекст <b>Чат {current_chat_slot}</b> для модели <b>{html.escape(model_id_to_name.get(current_model_id, "Неизвестная модель"))}</b> очищен.',
        parse_mode=ParseMode.HTML,
        reply_markup=get_main_buttons(user_id, current_model_id)
    )

# Обработчик кнопки "Продолжить" через CallbackQueryHandler
async def continue_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Обрабатывает нажатие кнопки "Продолжить" и отправляет дальнейший запрос модели.
    """
    query = update.callback_query
    await query.answer()

    user_id = query.from_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался использовать кнопку продолжить: {user_id}")
        await query.edit_message_text('тебе сюда мяукать нельзя.')
        return

    # Получаем текущую модель и чат пользователя
    current_model_id = user_state.get(user_id, {}).get('current_model_id')
    current_chat_slot = user_state.get(user_id, {}).get('current_chat_slot')

    if not current_model_id or not current_chat_slot:
        await query.edit_message_text('модель или чат не выбраны. используй /start для выбора модели и чата.')
        return

    # Проверка и инкремент лимита перед продолжением
    if not check_and_increment_limit(user_id, current_model_id):
        await query.edit_message_text('Вы достигли лимита запросов для этой модели.')
        return

    model_info = model_id_to_info.get(current_model_id, {})
    provider = model_info.get('provider', 'cablyai')
    selected_model_type = model_info.get('type', 'text').lower()

    # Проверка доступа к модели
    if current_model_id in RESTRICTED_MODELS and user_id not in AUTHORIZED_MODEL_USERS:
        await query.edit_message_text('у тебя нет доступа к этой модели.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

    # Получение текущего контекста
    context_messages = context_store.get(user_id, {}).get(current_model_id, {}).get(current_chat_slot, [])

    if not context_messages:
        await query.edit_message_text('Контекст чата пуст. Начните разговор с бота.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

    # Добавляем специальное сообщение для продолжения
    context_messages.append({"role": "user", "content": "Продолжи"})

    # Подготовка данных для API с учетом контекста
    if provider == 'cablyai':
        response_data = await send_cablyai_request(current_model_id, messages=context_messages, model_type=selected_model_type)
        if response_data and response_data.get('choices'):
            answer = response_data["choices"][0].get('message', {}).get('content', 'что-то пошло не так. мяу :(')
        else:
            logger.error("Неправильная структура ответа от CablyAI API.")
            await query.edit_message_text('API вернул непредвиденный ответ. Попробуй позже.', reply_markup=get_main_buttons(user_id, current_model_id))
            return
    elif provider == 'openai':
        response = await send_openai_request(current_model_id, messages=context_messages)
        if response and isinstance(response, dict):
            if 'choices' in response and len(response['choices']) > 0:
                if isinstance(response['choices'][0]['message']['content'], str):
                    answer = response["choices"][0].get('message', {}).get('content', 'что-то пошло не так. мяу :(')
                else:
                    # Если ответ содержит URL изображения
                    answer = response["choices"][0].get('message', {}).get('content', 'что-то пошло не так. мяу :(')
            else:
                logger.error("Неправильная структура ответа от OpenAI API.")
                await query.edit_message_text('API вернул непредвиденный ответ. Попробуй позже.', reply_markup=get_main_buttons(user_id, current_model_id))
                return
        else:
            logger.error("Неправильная структура ответа от OpenAI API.")
            await query.edit_message_text('API вернул непредвиденный ответ. попробуй потом.', reply_markup=get_main_buttons(user_id, current_model_id))
            return
    else:
        await query.edit_message_text('Неизвестный провайдер модели.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

    # Добавление ответа ассистента в контекст
    context_store[user_id][current_model_id][current_chat_slot].append({"role": "assistant", "content": answer})
    save_chat_context(user_id, current_model_id, current_chat_slot, context_store[user_id][current_model_id][current_chat_slot])

    try:
        if selected_model_type != 'image':
            # Преобразование Markdown в HTML
            answer_html = markdown_to_html(answer)
            messages = split_message(answer_html, MAX_MESSAGE_LENGTH)
            for msg in messages:
                # Отправляем все основные кнопки, включая "Продолжить"
                await query.message.reply_text(msg, reply_markup=get_main_buttons(user_id, current_model_id))
        # Не добавляем кнопку "Продолжить" для моделей типа 'image'
    except Exception as e:
        logger.error(f"Ошибка при отправке продолжения: {e}")
        await query.message.reply_text('Не удалось отправить продолжение ответа.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

# Функция для перевода текста с русского на английский с помощью модели CablyAI
async def translate_text(text):
    """
    Переводит текст с русского на английский с помощью модели chatgpt-4o-latest от CablyAI.
    """
    system_instruction = SYSTEM_INSTRUCTION_TEXT
    messages = [
        {"role": "system", "content": system_instruction},
        {"role": "user", "content": f"Переведи следующий текст на английский:\n\n{text}"}
    ]

    response = await send_cablyai_request("chatgpt-4o-latest", messages=messages, model_type='multimodal')
    if response and response.get('choices'):
        translated_text = response["choices"][0].get('message', {}).get('content', '').strip()
        return translated_text
    else:
        logger.error("Перевод текста не удался.")
        return None

# Обработчик сообщений
async def process_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_text = update.message.text
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text("У вас нет доступа к этому боту.")
        return

    # Получаем текущую модель и чат слота
    current_model_id = user_state.get(user_id, {}).get('current_model_id')
    current_chat_slot = user_state.get(user_id, {}).get('current_chat_slot')

    if not current_model_id or not current_chat_slot:
        await update.message.reply_text('Модель или чат не выбраны. Используйте /start для выбора модели и чата.', reply_markup=get_main_buttons(user_id))
        return

    model_info = model_id_to_info.get(current_model_id, {})
    provider = model_info.get('provider', 'cablyai')
    selected_model_type = model_info.get('type', 'text').lower()

    # Проверка доступа к модели
    if current_model_id in RESTRICTED_MODELS and user_id not in AUTHORIZED_MODEL_USERS:
        await update.message.reply_text('Вам нельзя использовать эту модель.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

    # Проверка и инкремент лимита
    if not check_and_increment_limit(user_id, current_model_id):
        await update.message.reply_text('Вы достигли лимита запросов для этой модели.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

    # Инициализация контекста, если ещё не инициализирован
    if user_id not in context_store:
        context_store[user_id] = {}
    if current_model_id not in context_store[user_id]:
        context_store[user_id][current_model_id] = {}
    if current_chat_slot not in context_store[user_id][current_model_id]:
        system_instruction = SYSTEM_INSTRUCTION_TEXT
        if provider == 'openai' and current_model_id not in ['openai-o1-preview', 'openai-o1-mini']:
            system_instruction = "You are a helpful assistant."
        context_store[user_id][current_model_id][current_chat_slot] = load_chat_context(user_id, current_model_id, current_chat_slot, system_instruction)

    if selected_model_type == 'image':
        # Проверка языка сообщения, теперь всегда переводим через модель при наличии текста
        if user_text:
            await update.message.reply_text("Твой запрос содержит русский язык. Генерация картинки будет идти чуть дольше - другой модели нужно перевести текст на английский для более корректного понимания моделью твоего запроса. Если тебя не устроила полученная картинка - ты всегда можешь сделать ещё запрос.")
            translated_text = await translate_to_english(user_text)
            if translated_text:
                user_text = translated_text
            else:
                await update.message.reply_text("Не удалось перевести текст. Пожалуйста, попробуйте позже.", reply_markup=get_main_buttons(user_id, current_model_id))
                return

        # Генерация или обработка изображения через соответствующий API
        image_or_answer = await process_image_request(user_id, current_model_id, user_text)
        if image_or_answer:
            if provider == 'cablyai':
                await update.message.reply_photo(photo=image_or_answer, reply_markup=get_main_buttons(user_id, current_model_id))
            elif provider == 'openai':
                if isinstance(image_or_answer, str) and image_or_answer.startswith('http'):
                    await update.message.reply_photo(photo=image_or_answer, reply_markup=get_main_buttons(user_id, current_model_id))
                else:
                    await update.message.reply_text(image_or_answer, reply_markup=get_main_buttons(user_id, current_model_id))
            elif provider == 'anthropic':
                await update.message.reply_text(image_or_answer, reply_markup=get_main_buttons(user_id, current_model_id))
        else:
            await update.message.reply_text("Не удалось сгенерировать/обработать изображение.", reply_markup=get_main_buttons(user_id, current_model_id))
    else:
        # Обработка текстовых сообщений для мультимодальных моделей
        context_store[user_id][current_model_id][current_chat_slot].append({"role": "user", "content": user_text})
        save_chat_context(user_id, current_model_id, current_chat_slot, context_store[user_id][current_model_id][current_chat_slot])

        # Подготовка данных для API с учетом контекста
        data = {
            "model": current_model_id,
            "messages": context_store[user_id][current_model_id][current_chat_slot]
        }

        if provider == 'cablyai':
            response_data = await send_cablyai_request(current_model_id, messages=data['messages'], model_type=selected_model_type)
            if response_data and response_data.get('choices'):
                answer = response_data["choices"][0].get('message', {}).get('content', 'Что-то пошло не так. Мяу :(')
            else:
                logger.error("Неправильная структура ответа от CablyAI API.")
                await update.message.reply_text('API вернул непредвиденный ответ. Попробуй позже.', reply_markup=get_main_buttons(user_id, current_model_id))
                return
        elif provider == 'openai':
            response = await send_openai_request(current_model_id, messages=data['messages'])
            if response and isinstance(response, dict):
                if 'choices' in response and len(response['choices']) > 0:
                    if isinstance(response['choices'][0]['message']['content'], str):
                        answer = response["choices"][0].get('message', {}).get('content', 'Что-то пошло не так. Мяу :(')
                    else:
                        # Если ответ содержит URL изображения
                        answer = response["choices"][0].get('message', {}).get('content', 'Что-то пошло не так. Мяу :(')
                else:
                    logger.error("Неправильная структура ответа от OpenAI API.")
                    await update.message.reply_text('API вернул непредвиденный ответ. попробуй потом.', reply_markup=get_main_buttons(user_id, current_model_id))
                    return
            else:
                logger.error("Неправильная структура ответа от OpenAI API.")
                await update.message.reply_text('API вернул непредвиденный ответ. попробуй потом.', reply_markup=get_main_buttons(user_id, current_model_id))
                return
        else:
            await update.message.reply_text('Неизвестный провайдер модели.', reply_markup=get_main_buttons(user_id, current_model_id))
            return

        # Добавление ответа ассистента в контекст
        context_store[user_id][current_model_id][current_chat_slot].append({"role": "assistant", "content": answer})
        save_chat_context(user_id, current_model_id, current_chat_slot, context_store[user_id][current_model_id][current_chat_slot])

        if user_text.lower().strip() != "продолжи":
            if user_mode_store.get(str(user_id), 'message') == 'message':
                # Преобразование Markdown в HTML
                answer_html = markdown_to_html(answer)
                messages = split_message(answer_html, MAX_MESSAGE_LENGTH)
                for msg in messages:
                    # Отправляем все основные кнопки, включая "Продолжить"
                    await update.message.reply_text(msg, reply_markup=get_main_buttons(user_id, current_model_id))
            elif user_mode_store.get(str(user_id), 'file') == 'file':
                # Сохранение ответа в .md файл
                file_path = save_md_file(answer, user_id)
                if file_path:
                    # Отправка файла пользователю
                    try:
                        with open(file_path, 'rb') as f:
                            await update.message.reply_document(
                                document=InputFile(f),
                                filename=os.path.basename(file_path),
                                caption="Твой ответ модели в формате markdown.",
                                reply_markup=get_main_buttons(user_id, current_model_id)
                            )
                        # Удаление файла после отправки
                        os.remove(file_path)
                    except Exception as e:
                        logger.error(f"Ошибка при отправке файла: {e}")
                        await update.message.reply_text('Не удалось отправить файл. Попробуйте позже.', reply_markup=get_main_buttons(user_id, current_model_id))
                else:
                    await update.message.reply_text('Не удалось создать файл с ответом.', reply_markup=get_main_buttons(user_id, current_model_id))

# Обработчик фотографий
async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь отправил изображение: {user_id}")
        await update.message.reply_text('Тебе сюда мяукать нельзя.')
        return

    # Получаем текущую модель и чат слот
    current_model_id = user_state.get(user_id, {}).get('current_model_id')
    current_chat_slot = user_state.get(user_id, {}).get('current_chat_slot')

    if not current_model_id or not current_chat_slot:
        await update.message.reply_text('Модель или чат не выбраны. Используйте /start для выбора модели и чата.', reply_markup=get_main_buttons(user_id))
        return

    model_info = model_id_to_info.get(current_model_id, {})
    provider = model_info.get('provider', 'cablyai')
    selected_model_type = model_info.get('type', 'text').lower()

    # Проверка доступа к модели
    if current_model_id in RESTRICTED_MODELS and user_id not in AUTHORIZED_MODEL_USERS:
        await update.message.reply_text('Вам нельзя использовать эту модель.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

    # Проверка и инкремент лимита
    if not check_and_increment_limit(user_id, current_model_id):
        await update.message.reply_text('Вы достигли лимита запросов для этой модели.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

    # Проверка поддержки изображений
    if not is_image_supported(current_model_id):
        await update.message.reply_text('Выбранная модель не поддерживает обработку изображений.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

    # Скачиваем изображение
    photo = update.message.photo[-1]  # Берем наибольшее разрешение изображения
    file = await photo.get_file()
    downloads_photos_dir = os.path.join('downloads', 'photos')
    if not os.path.exists(downloads_photos_dir):
        os.makedirs(downloads_photos_dir)
    file_path = os.path.join(downloads_photos_dir, f"{user_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}.jpg")

    try:
        await file.download_to_drive(file_path)
    except Exception as e:
        logger.error(f"Ошибка при скачивании изображения: {e}")
        await update.message.reply_text('Не удалось скачать изображение. Попробуй позже.', reply_markup=get_main_buttons(user_id))
        return

    # Подготовка промпта из caption или пустой строки
    user_caption = update.message.caption or ""

    # Проверка языка промпта, теперь всегда переводим через модель при наличии текста
    if user_caption:
        await update.message.reply_text("Пожалуйста, подождите, ваш запрос обрабатывается...")
        translated_text = await translate_to_english(user_caption)
        if translated_text:
            user_caption = translated_text
        else:
            await update.message.reply_text("Не удалось перевести текст. Пожалуйста, попробуйте позже.", reply_markup=get_main_buttons(user_id, current_model_id))
            return

    # Генерация или обработка изображения через соответствующий API
    image_or_answer = await process_image_request(user_id, current_model_id, user_caption, image_path=file_path)
    if image_or_answer:
        if provider == 'cablyai':
            await update.message.reply_photo(photo=image_or_answer, reply_markup=get_main_buttons(user_id, current_model_id))
        elif provider == 'openai':
            if isinstance(image_or_answer, str) and image_or_answer.startswith('http'):
                await update.message.reply_photo(photo=image_or_answer, reply_markup=get_main_buttons(user_id, current_model_id))
            else:
                await update.message.reply_text(image_or_answer, reply_markup=get_main_buttons(user_id, current_model_id))
        elif provider == 'anthropic':
            await update.message.reply_text(image_or_answer, reply_markup=get_main_buttons(user_id, current_model_id))
    else:
        await update.message.reply_text("Не удалось сгенерировать или обработать изображение.", reply_markup=get_main_buttons(user_id, current_model_id))

    # Удаление скачанного изображения
    if os.path.exists(file_path):
        os.remove(file_path)

# Обработчик документов (файлов)
async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался отправить файл: {user_id}")
        await update.message.reply_text('Тебе сюда мяукать нельзя.')
        return

    user_mode = user_mode_store.get(str(user_id), 'message')

    document = update.message.document
    file = await document.get_file()
    filename = document.file_name.lower()  # Приводим к нижнему регистру для надёжной проверки

    # Проверка, является ли файл .py или .json
    if filename.endswith('.py') or filename.endswith('.json'):
        # Обработка как текстовый файл
        try:
            # Создаём директорию для скачивания, если её нет
            download_path = os.path.join('downloads', 'files')
            if not os.path.exists(download_path):
                os.makedirs(download_path)

            # Скачиваем файл
            file_path = os.path.join(download_path, document.file_name)
            await file.download_to_drive(file_path)

            # Читаем содержимое файла
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()

            # Удаляем файл после чтения
            os.remove(file_path)
        except Exception as e:
            logger.error(f"Ошибка при скачивании или чтении файла: {e}")
            await update.message.reply_text('Не удалось скачать или прочитать файл. Попробуйте позже.', reply_markup=get_main_buttons(user_id))
            return

        # Получаем текущую модель и чат слота
        current_model_id = user_state.get(user_id, {}).get('current_model_id')
        current_chat_slot = user_state.get(user_id, {}).get('current_chat_slot')

        if not current_model_id or not current_chat_slot:
            await update.message.reply_text('Модель или чат не выбраны. Используйте /start для выбора модели и чата.', reply_markup=get_main_buttons(user_id))
            return

        model_info = model_id_to_info.get(current_model_id, {})
        provider = model_info.get('provider', 'cablyai')

        # Проверка доступа к модели
        if current_model_id in RESTRICTED_MODELS and user_id not in AUTHORIZED_MODEL_USERS:
            await update.message.reply_text('Вам нельзя использовать эту модель.', reply_markup=get_main_buttons(user_id, current_model_id))
            return

        # Проверка, поддерживает ли модель текстовый ввод
        selected_model_type = model_info.get('type', 'text').lower()
        if selected_model_type not in ['text', 'multimodal']:
            await update.message.reply_text('Выбранная модель не поддерживает обработку текстовых файлов.', reply_markup=get_main_buttons(user_id, current_model_id))
            return

        # Проверка и инкремент лимита
        if not check_and_increment_limit(user_id, current_model_id):
            await update.message.reply_text('Вы достигли лимита запросов для этой модели.', reply_markup=get_main_buttons(user_id, current_model_id))
            return

        # Инициализация контекста, если ещё не инициализирован
        if user_id not in context_store:
            context_store[user_id] = {}
        if current_model_id not in context_store[user_id]:
            context_store[user_id][current_model_id] = {}
        if current_chat_slot not in context_store[user_id][current_model_id]:
            system_instruction = SYSTEM_INSTRUCTION_TEXT
            if provider == 'openai' and current_model_id not in ['openai-o1-preview', 'openai-o1-mini']:
                system_instruction = "You are a helpful assistant."
            context_store[user_id][current_model_id][current_chat_slot] = load_chat_context(user_id, current_model_id, current_chat_slot, system_instruction)

        # Добавление содержимого файла в контекст
        context_store[user_id][current_model_id][current_chat_slot].append({"role": "user", "content": content})

        save_chat_context(user_id, current_model_id, current_chat_slot, context_store[user_id][current_model_id][current_chat_slot])

        # Подготовка данных для API с учетом контекста
        data = {
            "model": current_model_id,
            "messages": context_store[user_id][current_model_id][current_chat_slot]
        }

        if provider == 'cablyai':
            response_data = await send_cablyai_request(current_model_id, messages=data['messages'], model_type=selected_model_type)
            if response_data and response_data.get('choices'):
                answer = response_data["choices"][0].get('message', {}).get('content', 'Что-то пошло не так. Мяу :(')
            else:
                logger.error("Неправильная структура ответа от CablyAI API.")
                await update.message.reply_text('API вернул непредвиденный ответ. Попробуй позже.', reply_markup=get_main_buttons(user_id, current_model_id))
                return
        elif provider == 'openai':
            response = await send_openai_request(current_model_id, messages=data['messages'])
            if response and isinstance(response, dict):
                if 'choices' in response and len(response['choices']) > 0:
                    if isinstance(response['choices'][0]['message']['content'], str):
                        answer = response["choices"][0].get('message', {}).get('content', 'Что-то пошло не так. Мяу :(')
                    else:
                        # Если ответ содержит URL изображения
                        answer = response["choices"][0].get('message', {}).get('content', 'Что-то пошло не так. Мяу :(')
                else:
                    logger.error("Неправильная структура ответа от OpenAI API.")
                    await update.message.reply_text('API вернул непредвиденный ответ. Попробуй позже.', reply_markup=get_main_buttons(user_id, current_model_id))
                    return
            else:
                logger.error("Неправильная структура ответа от OpenAI API.")
                await update.message.reply_text('API вернул непредвиденный ответ. Попробуй позже.', reply_markup=get_main_buttons(user_id, current_model_id))
                return
        else:
            await update.message.reply_text('Неизвестный провайдер модели.', reply_markup=get_main_buttons(user_id, current_model_id))
            return

        # Добавление ответа ассистента в контекст
        context_store[user_id][current_model_id][current_chat_slot].append({"role": "assistant", "content": answer})
        save_chat_context(user_id, current_model_id, current_chat_slot, context_store[user_id][current_model_id][current_chat_slot])

        if user_mode == 'message':
            # Преобразование Markdown в HTML
            answer_html = markdown_to_html(answer)
            messages = split_message(answer_html, MAX_MESSAGE_LENGTH)
            for msg in messages:
                # Отправляем все основные кнопки, включая "Продолжить"
                await update.message.reply_text(msg, reply_markup=get_main_buttons(user_id, current_model_id))
        elif user_mode == 'file':
            # Сохранение ответа в .md файл
            file_path = save_md_file(answer, user_id)
            if file_path:
                # Отправка файла пользователю
                try:
                    with open(file_path, 'rb') as f:
                        await update.message.reply_document(
                            document=InputFile(f),
                            filename=os.path.basename(file_path),
                            caption="Твой ответ модели в формате markdown.",
                            reply_markup=get_main_buttons(user_id, current_model_id)
                        )
                    # Удаление файла после отправки
                    os.remove(file_path)
                except Exception as e:
                    logger.error(f"Ошибка при отправке файла: {e}")
                    await update.message.reply_text('Не удалось отправить файл. Попробуйте позже.', reply_markup=get_main_buttons(user_id, current_model_id))
            else:
                await update.message.reply_text('Не удалось создать файл с ответом.', reply_markup=get_main_buttons(user_id, current_model_id))
    else:
        await update.message.reply_text('Поддерживаются только файлы с расширениями .py и .json.', reply_markup=get_main_buttons(user_id))

# Обработчик кнопок
async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    query = update.callback_query
    data = query.data

    if data == "continue":
        await continue_callback(update, context)
    else:
        # Обработка других кнопок, если необходимо
        pass

# Обработчик команды /limits
async def limits_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Выводит пользователю информацию о его лимитах и оставшихся запросах на сегодня.
    """
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text("У вас нет доступа к этому боту.")
        return

    user_id_str = str(user_id)
    user_limits = config.get('user_limits', {}).get(user_id_str, {})
    if not user_limits:
        await update.message.reply_text("У вас нет установленных лимитов.", reply_markup=get_main_buttons(user_id))
        return

    response_lines = ["<b>Ваши лимиты на сегодня:</b>"]
    for model_id, limit in user_limits.items():
        usage = usage_store.get(user_id_str, {}).get(model_id, 0)
        if limit == "infinite":
            remaining = "∞"
        else:
            remaining = limit - usage
            if remaining < 0:
                remaining = 0
        model_name = model_id_to_name.get(model_id, model_id)
        response_lines.append(f"- <b>{html.escape(model_name)}</b>: {usage} / {limit} запросов (Осталось: {remaining})")

    response_text = "\n".join(response_lines)
    response_text += f"\n\n<code>Лимиты сбросятся в {config.get('limit_reset_time', '00:00')} по московскому времени.</code>"

    await update.message.reply_text(response_text, parse_mode=ParseMode.HTML)

# Обработчик команды /clearlimits
async def clearlimits_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Очищает все лимиты пользователей на сегодня. Доступна только администратору.
    """
    user_id = update.effective_user.id
    admin_id = 5005816138
    if user_id != admin_id:
        await update.message.reply_text("У вас нет прав для использования этой команды.")
        return

    # Очистка usage_store
    usage_store.clear()
    save_usage()

    # Отправка уведомлений всем пользователям о сбросе лимитов
    for uid in AUTHORIZED_USER_IDS:
        try:
            await context.bot.send_message(
                chat_id=uid,
                text="Администратор сбросил ваши лимиты на сегодня."
            )
        except Exception as e:
            logger.error(f"Не удалось отправить сообщение пользователю {uid}: {e}")

    await update.message.reply_text("Все лимиты пользователей сброшены.")

# Обработчик ошибок
async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Ловит все необработанные ошибки."""
    logger.error(msg="Exception while handling an update:", exc_info=context.error)
    if isinstance(update, Update) and update.effective_user:
        try:
            user_id = update.effective_user.id
            # Получаем текущую модель, если она есть
            current_model_id = user_state.get(user_id, {}).get('current_model_id')
            await update.effective_message.reply_text('Произошла ошибка. Пожалуйста, попробуйте позже.', reply_markup=get_main_buttons(user_id, current_model_id))
        except TelegramError as e:
            logger.error(f"Не удалось отправить сообщение об ошибке пользователю: {e}")

# Фоновая задача для сброса лимитов в 00:00 по московскому времени
async def reset_limits_daily():
    while True:
        now = datetime.now(ZoneInfo('Europe/Moscow'))
        next_reset = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
        sleep_duration = (next_reset - now).total_seconds()
        logger.info(f"Сброс лимитов будет через {sleep_duration} секунд.")
        await asyncio.sleep(sleep_duration)

        # Сброс лимитов
        usage_store.clear()
        save_usage()
        logger.info("Лимиты пользователей сброшены.")

        # Отправка уведомлений всем пользователям о сбросе лимитов
        for uid in AUTHORIZED_USER_IDS:
            try:
                await application.bot.send_message(
                    chat_id=uid,
                    text="Дневные лимиты сброшены. Вы можете продолжать использовать бота."
                )
            except Exception as e:
                logger.error(f"Не удалось отправить сообщение пользователю {uid}: {e}")

# Основная функция запуска бота
def main():
    global application  # Чтобы фоновая задача могла использовать объект application

    TELEGRAM_TOKEN = config.get('telegram_token')
    if not TELEGRAM_TOKEN:
        logger.error("Токен бота не указан в config.json.")
        exit(1)

    # Создание директорий для чатов, если её нет
    chats_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'chats')
    if not os.path.exists(chats_dir):
        os.makedirs(chats_dir)

    # Создание директорий для загрузок, если их нет
    downloads_photos_dir = os.path.join('downloads', 'photos')
    if not os.path.exists(downloads_photos_dir):
        os.makedirs(downloads_photos_dir)

    downloads_files_dir = os.path.join('downloads', 'files')
    if not os.path.exists(downloads_files_dir):
        os.makedirs(downloads_files_dir)

    # Создание директорий для ответов и загрузок, если их нет
    responses_dir = os.path.join('responses')
    if not os.path.exists(responses_dir):
        os.makedirs(responses_dir)

    # Создание экземпляра приложения с установкой ParseMode.HTML по умолчанию
    application = ApplicationBuilder().token(TELEGRAM_TOKEN).defaults(Defaults(parse_mode=ParseMode.HTML)).build()

    # Обработчики команд
    application.add_handler(CommandHandler("start", start))
    application.add_handler(CommandHandler("setmode", setmode))
    application.add_handler(CommandHandler("mode", mode))
    application.add_handler(CommandHandler("instructions", instructions))
    application.add_handler(CommandHandler("models", models_command))
    application.add_handler(CommandHandler("reload", reload_bot))
    application.add_handler(CommandHandler("limits", limits_command))
    application.add_handler(CommandHandler("clearlimits", clearlimits_command))

    # Обработчики CallbackQuery
    application.add_handler(CallbackQueryHandler(select_model, pattern='^model_select:.+$|^restricted:.+$'))
    application.add_handler(CallbackQueryHandler(select_chat_slot, pattern='^chat_slot:\d+:.+$'))
    application.add_handler(CallbackQueryHandler(change_model_callback, pattern='^change_model$'))
    application.add_handler(CallbackQueryHandler(change_chat_slot_callback, pattern='^change_chat_slot$'))
    application.add_handler(CallbackQueryHandler(clear_context_callback, pattern='^clear_context$'))
    application.add_handler(CallbackQueryHandler(button_handler, pattern='^continue$'))

    # Обработчики сообщений
    application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, process_message))
    application.add_handler(MessageHandler(filters.PHOTO, handle_photo))
    application.add_handler(MessageHandler(filters.Document.ALL, handle_document))

    # Обработчик ошибок
    application.add_error_handler(error_handler)

    # Запуск фоновой задачи для сброса лимитов
    application.job_queue.run_once(lambda context: asyncio.create_task(reset_limits_daily()), when=0)

    # Запуск бота
    application.run_polling()

if __name__ == '__main__':
    main()