сделай мне чтобы у меня если первое апи закончилось ко второму переход был, и так далее, по кругу. и возле GEMINI писалось (pol3) если юзается щас это апи. как понять что апи закончилось? ошибка выходит такая Error: 429 Resource has been exhausted (e.g. check quota)


2 файла
в одном апи
API_KEYS = [
    "pol3": 'AIzaSyBrPwohj6xh7aXmKOCb6E_lpqSIyd19Hq0',  # fyafy
    "new_pos": 'AIzaSyBUICcM0_GdFPcCLydkwYz4VrT8P6_02t8',  # polik
    # 'AIzaSyDdP1r6a0mpKYM0tAVcBz2Z_Bb9g8kbhSw',  # smile
]

в другом код:

import enum
import io
import os
import re
import tempfile
import subprocess
from decimal import Decimal
from typing import List, Optional, Tuple

import openpyxl
import docx2txt
import PyPDF2

import aiohttp
from aiogram import Router, F, Bot, types, html
from aiogram.filters import Command, CommandObject
from aiogram.filters.chat_member_updated import ChatMemberUpdatedFilter, MEMBER, ADMINISTRATOR, CREATOR
from aiogram.types import (
    Message,
    InlineKeyboardButton,
    InputMediaPhoto,
    BufferedInputFile
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bs4 import BeautifulSoup
from cachetools import TTLCache
from playwright.async_api import async_playwright

import google.generativeai as genai
from google.generativeai.types import HarmCategory, HarmBlockThreshold
from google.generativeai.types.content_types import to_contents
from google.generativeai.types.generation_types import BlockedPromptException

from config.settings import TOKEN_GEMINI
from db_connect import create_db_connection
from filters.owner import OwnerList
from filters.protect_button import ProtectButton
from utils.youtube_subtitle import get_youtube_transcript, extract_video_id


router_GPT = Router()
router_GPT.callback_query.filter(ProtectButton())

# --- Constants ---
CACHE_TTL = 3600
CACHE_MAXSIZE = 1000
MAX_SYSTEM_INSTRUCTION_LENGTH = 500
RESPONSE_CHUNK_SIZE = 4000
RESPONSE_TRIM_LENGTH = 450

# Замените на ID вашего канала (например, -100123456789)
CHANNEL_ID = -1001516772908
# Замените на ID вашей группы (например, -100987654321)
GROUP_ID = -1001682604832

# --- Global Variables ---
# ---


async def check_subscription(bot: Bot, user_id: int) -> bool:
    try:
        member = await bot.get_chat_member(chat_id=CHANNEL_ID, user_id=user_id)
        return member.status.value in ['administrator', 'member', 'owner']
    except Exception:
        return False


async def get_text_content_with_playwright(url):
    async with async_playwright() as p:
        try:
            browser = await p.chromium.launch(headless=True)
            page = await browser.new_page()
            await page.goto(url)

            # Ждем, пока не появится какой-то текст на странице
            await page.wait_for_selector("body:not(:empty)")

            # Получаем текст всего body
            page_text = await page.locator("body").inner_text()

            return page_text

        except Exception:
            return None

        finally:
            await browser.close()

# --- Enums ---
class GPTModel(enum.Enum):
    GPT_FOR_INTERNET = "gpt-for-internet"
    GEMINI = "gemini"

    @property
    def display_name(self) -> str:
        return self.value.upper()

class GeminiAPI:
    def __init__(self):
        pass

    def _upload_to_gemini(self, file_data: io.BytesIO, mime_type: str):
        """Загружает файл в Google Gemini."""
        try:
            with tempfile.NamedTemporaryFile(delete=False) as temp_file:
                temp_file_path = temp_file.name
                if isinstance(file_data, io.StringIO):
                    temp_file.write(file_data.read().encode())
                else:
                    temp_file.write(file_data.read())
            return genai.upload_file(
                path=temp_file_path, mime_type=mime_type
            )
        except Exception as e:
            print(f"Error uploading file to Gemini: {e}")
            raise
        finally:
            if os.path.exists(temp_file_path):
                os.remove(temp_file_path)

    async def get_gemini_answer(self, text, message_history=[], system_instruction=None, model_name="gemini-1.5-pro-exp-0827", response_mime_type="text/plain"):
        generation_config = {
            "temperature": 0,
            "top_p": 0.8,
            "top_k": 30,
            "max_output_tokens": 1874901405,
            "response_mime_type": response_mime_type,
        }

        model = genai.GenerativeModel(
            model_name=model_name,
            system_instruction=system_instruction,
            generation_config=generation_config,
            safety_settings={
                HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
                HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
                HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
                HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
            },
        )

        # Преобразование текста и истории сообщений в строки, если необходимо
        if not isinstance(text, str):
            text = str(text)
        
        message_history_texts = [
            str(item) if not isinstance(item, str) else item
            for item in message_history
        ]

        prompt_tokens = model.count_tokens(
            message_history_texts + [text]
        ).total_tokens

        chat_session = model.start_chat(history=message_history)

        try:
            response = chat_session.send_message(text)
            response_text = response.text
            response_tokens = response.usage_metadata.candidates_token_count

        except BlockedPromptException as e:
            print(f"BlockedPromptException: {e}")
            # Instead of accessing e.block_reason, we'll use the exception message
            block_reason = str(e)
            print(f"Block reason: {block_reason}")

            # Check if any of the harm categories are mentioned in the exception message
            harm_categories = [
                HarmCategory.HARM_CATEGORY_HATE_SPEECH,
                HarmCategory.HARM_CATEGORY_HARASSMENT,
                HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
                HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT
            ]
            
            if any(category.name in block_reason for category in harm_categories):
                try:
                    with tempfile.NamedTemporaryFile(delete=False, suffix='.txt') as temp_file:
                        temp_file.write(text.encode('utf-8'))
                        temp_file_path = temp_file.name

                    uploaded_file = genai.upload_file(path=temp_file_path, mime_type='text/plain')
                    response = chat_session.send_message(uploaded_file)
                    response_text = response.text
                    response_tokens = response.usage_metadata.candidates_token_count

                except BlockedPromptException as e:
                    print(f"BlockedPromptException on retry: {e}")
                    return f"Error: {str(e)}", prompt_tokens, 0
                finally:
                    if os.path.exists(temp_file_path):
                        os.remove(temp_file_path)
            else:
                return f"Error: {block_reason}", prompt_tokens, 0
        except Exception as e:
            print(f"Unexpected error: {e}")
            return f"Error: {str(e)}", 0, 0

        return response_text, prompt_tokens, response_tokens


gemini_api = GeminiAPI()

# --- Service Class ---
class GPTService:
    def __init__(self, proxy=None):
        self.proxy = proxy
        self.gemini_api = gemini_api

    async def get_morphic_answer(
        self, question: str, retries: int = 5
    ) -> Tuple[str, List]:
        # Перезапускаем службу Tor
        subprocess.run(['sudo', 'systemctl', 'restart', 'tor'])
        """Retrieves an answer from Morphic AI for the given question.

        Args:
            question (str): The question to ask Morphic AI.
            retries (int, optional): The number of times to retry the request. Defaults to 5.

        Returns:
            Tuple[str, List]: A tuple containing the answer text and a list of image URLs.
        """
        for attempt in range(retries):
            try:
                async with async_playwright() as p:
                    browser = await p.chromium.launch(
                        headless=True,
                        proxy={"server": "socks5://127.0.0.1:9050"},
                        args=[
                            "--ignore-certificate-errors",
                            "--disable-web-security",
                            "--disable-gpu",
                            "--disable-dev-shm-usage",
                            "--no-sandbox",
                            "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
                        ],
                    )
                    page = await browser.new_page()
                    await page.goto("https://www.morphic.sh/")
                    await page.wait_for_load_state("networkidle")

                    input_field = page.locator('textarea[name="input"]')
                    await input_field.fill(question)
                    await input_field.press("Enter")

                    await page.wait_for_selector(
                        'div[data-orientation="horizontal"][role="none"].shrink-0.h-\\[1px\\].w-full.my-2.bg-primary\\/10',
                        timeout=30000,
                    )

                    answer_element = await page.wait_for_selector(
                        "div.prose-sm"
                    )
                    return await self._process_answer_content(
                        answer_element
                    )
            except Exception as e:
                print(
                    f"Error in get_morphic_answer (attempt {attempt + 1}): {e}"
                )
        return "Error: Could not get a response.", []

    async def _process_answer_content(self, answer_element):
        """Processes the answer content from Morphic AI.

        Args:
            answer_element: The answer element from the Morphic AI response.

        Returns:
            Tuple[str, List]: A tuple containing the processed answer text and a list of image URLs.
        """
        soup = BeautifulSoup(
            await answer_element.inner_html(), "html.parser"
        )
        image_urls = [
            img["src"] for img in soup.find_all("img", src=True)
        ]
        for tag in soup.find_all(lambda tag: tag.name != "a"):
            tag.unwrap()
        answer_text = str(soup).strip()
        return answer_text, image_urls

    async def get_gemini_answer(
        self,
        context: dict,
        user_id: int,
        message: types.Message = None,
        bot: Bot = None,
    ) -> str:
        """Retrieves an answer from Google Gemini for the given text.

        Args:
            text (str): The text to send to Google Gemini.
            user_id (int): The ID of the user making the request.
            message (types.Message, optional): The message object. Defaults to None.
            bot (Bot, optional): The bot object. Defaults to None.

        Returns:
            str: The formatted response from Google Gemini.
        """
        async with create_db_connection() as conn:
            system_instruction = await conn.fetchval(
                "SELECT system_instruction FROM chats WHERE user_id = $1",
                user_id,
            )
            history = message_history_cache.get(user_id, [])
            model_name = "gemini-1.5-pro-latest"
            text = context['user_text'] if 'user_text' in context else ''

            if message:
                reply = message.reply_to_message
                if reply and (reply.photo or reply.document or (reply.audio or reply.voice)):
                    message = reply

                if message.photo or message.document:
                    # history = []

                    if message.photo:
                        file_id = message.photo[-1].file_id
                        mime_type = "image/jpeg"
                    else:  # message.document
                        # model_name = "gemini-1.5-flash-latest"
                        file_id = message.document.file_id
                        mime_type = message.document.mime_type or "application/octet-stream"

                    file = await bot.get_file(file_id)
                    file_bytes = await bot.download_file(file.file_path)

                    # Определение типа файла и конвертация в текст, если необходимо
                    file_ext = os.path.splitext(file.file_path)[1].lower()
                    if file_ext in ['.doc', '.docx', '.ppt', '.pptx']:
                        extracted_text = docx2txt.process(io.BytesIO(file_bytes.read()))
                        uploaded_file = self._upload_to_gemini(io.StringIO(extracted_text), mime_type="text/plain")
                    elif file_ext == '.pdf':
                        pdf_reader = PyPDF2.PdfReader(io.BytesIO(file_bytes.read()))
                        extracted_text = ""
                        for page_num in range(len(pdf_reader.pages)):
                            page = pdf_reader.pages[page_num]
                            extracted_text += page.extract_text()
                        uploaded_file = self._upload_to_gemini(io.StringIO(extracted_text), mime_type="text/plain")
                    elif file_ext in ['.xlsx', '.xlsm']:
                        workbook = openpyxl.load_workbook(io.BytesIO(file_bytes.read()))
                        sheet = workbook.active
                        table_text = ""
                        for row in sheet.iter_rows():
                            row_text = ""
                            for cell in row:
                                if cell.value is not None:
                                    row_text += str(cell.value) + "\t"
                            table_text += row_text.strip() + "\n"
                        uploaded_file = self._upload_to_gemini(io.StringIO(table_text), mime_type="text/plain")
                    else:
                        uploaded_file = self._upload_to_gemini(io.BytesIO(file_bytes.read()), mime_type=mime_type)
                    history.append({"role": "user", "parts": [uploaded_file]})
                elif message.voice or message.audio:
                    # model_name = "gemini-1.5-flash-latest"
                    history = []
                    voice = message.voice if message.voice else message.audio
                    file = await bot.get_file(voice.file_id)
                    audio_bytes = await bot.download_file(file.file_path)
                    uploaded_file = self._upload_to_gemini(io.BytesIO(audio_bytes.read()), mime_type="audio/ogg")
                    history.append({"role": "user", "parts": [uploaded_file]})
                else:
                    if "youtube_transcript_file" in context:
                        transcript_file = context["youtube_transcript_file"]
                        uploaded_file = self._upload_to_gemini(transcript_file, mime_type="text/plain")
                        history.append({"role": "user", "parts": [uploaded_file]})
                        if "question" in context:
                            text = context['question']
                            system_instruction = (
                                f"Answer only the question, should be in '{message.from_user.language_code}' language."
                            )
                        else:
                            system_instruction = (
                                f"Make a summary of this video. I am providing you with the subtitles from the video. The summary should be in '{message.from_user.language_code}' language, focusing on the nuances and key moments."
                            )
                            # model_name = "gemini-1.5-flash-latest"
                    elif "webpage_content" in context:
                        text = context['webpage_content']
                        if "question" in context:
                            system_instruction = (
                                f"Answer only the question based on the content of this webpage. The answer should be in '{message.from_user.language_code}' language."
                            )
                        else:
                            system_instruction = (
                                f"Summarize the content of this webpage. The summary should be in '{message.from_user.language_code}' language."
                            )
                        # model_name = "gemini-1.5-flash-latest"
                    elif "quoted_text" in context:
                        text = context['quoted_text']
                        if "question" in context:
                            text = f"'{context['quoted_text']}', \n\n{context['question']}"
                    history.append({"role": "user", "parts": [{"text": text}]})
            text = text if text not in ['', None] else text + "."
            response_text, prompt_tokens, response_tokens = await self.gemini_api.get_gemini_answer(
                text, history, system_instruction, model_name
            )

            user_data = await conn.fetchrow(
                "SELECT tokens_used, cost FROM users_token WHERE user_id = $1",
                user_id,
            )
            if not user_data:
                await conn.execute(
                    "INSERT INTO users_token (user_id, tokens_used, cost) VALUES ($1, $2, $3)",
                    user_id,
                    0,
                    0.00,
                )
                user_tokens_used = Decimal(0)
                user_cost = Decimal(0)
            else:
                user_tokens_used = Decimal(user_data[0])
                user_cost = Decimal(user_data[1])

            user_tokens_used += Decimal(prompt_tokens + response_tokens)
            user_cost += Decimal(self.calculate_cost(prompt_tokens + response_tokens))

            await conn.execute(
                "UPDATE users_token SET tokens_used = $1, cost = $2 WHERE user_id = $3",
                user_tokens_used,
                user_cost,
                user_id,
            )
            total_data = await conn.fetchrow(
                "SELECT SUM(tokens_used), SUM(cost) FROM users_token"
            )
            total_tokens_used, total_cost = total_data if total_data else (0, 0.00)

        history.append({"role": "model", "parts": [{"text": response_text}]})
        message_history_cache[user_id] = history

        def shorten_number(num):
            if num < 1000:
                return str(num)
            magnitude_lookup = ['', '', 'M', 'B', 'T', 'P', 'E', 'Z', 'Y']
            magnitude = 0
            while num >= 1000 and magnitude < len(magnitude_lookup) - 1:
                num /= 1000
                magnitude += 1
            return f"{num:.3f}".rstrip('0').rstrip('.') + magnitude_lookup[magnitude]

        token_info = (
            f"Tokens: 👤 {prompt_tokens} | {response_tokens} 🤖\n"
            f"Total: 👤{shorten_number(user_tokens_used)} | {shorten_number(total_tokens_used)} 👥\n"
            f"Estimated cost: 👤 ${user_cost:.2f} | ${total_cost:.2f} 👥"
        )

        return f"{self.format_response(html.quote(response_text))[:-1]}\n\n{token_info}"

    def _upload_to_gemini(
        self, file_data: io.BytesIO, mime_type: str
    ):
        """Uploads a file to Google Gemini.

        Args:
            file_data (io.BytesIO): The file data to upload.
            mime_type (str): The MIME type of the file.

        Returns:
            The uploaded file object.
        """
        try:
            with tempfile.NamedTemporaryFile(delete=False) as temp_file:
                temp_file_path = temp_file.name
                if isinstance(file_data, io.StringIO):
                    temp_file.write(file_data.read().encode()) # Кодируем строку в байты
                else:
                    temp_file.write(file_data.read())
            return genai.upload_file(
                path=temp_file_path, mime_type=mime_type
            )
        except Exception as e:
            print(f"Error uploading file to Gemini: {e}")
            raise
        finally:
            if os.path.exists(temp_file_path):
                os.remove(temp_file_path)

    @staticmethod
    def format_response(response: str):
        """Formats the response text from Google Gemini.

        Args:
            response (str): The response text to format.

        Returns:
            str: The formatted response text.
        """
        response = re.sub(
            r"\* \*\*(.*?)\*\*", r"● <b>\1</b>", response
        )
        response = re.sub(r"\*\*(.*?)\*\*", r"<b>\1</b>", response)
        response = re.sub(r"^#### (.*?)", r"◉ \1", response)
        response = re.sub(r"^## (.*?)", r"● <b>\1</b>", response)
        response = re.sub(r"^\s*\* (.*?)", r"• \1", response, flags=re.DOTALL | re.MULTILINE)
        response = re.sub(
            r"```(?:([\w-]+)\n)?(.*?)```",
            lambda m: f'<pre><code class="language-{m.group(1) or ""}">{m.group(2)}</code></pre>',
            response,
            flags=re.DOTALL,
        )
        response = re.sub(r"`(.+?)`", r"<code>\1</code>", response)
        return response

    @staticmethod
    def calculate_cost(tokens_used: int) -> Decimal:
        """Calculates the cost of the request based on the number of tokens used.

        Args:
            tokens_used (int): The number of tokens used.

        Returns:
            Decimal: The calculated cost.
        """
        if tokens_used <= 128000:
            return (Decimal(tokens_used) / Decimal(1_000_000)) * Decimal(
                "3.5"
            )
        return (Decimal(tokens_used) / Decimal(1_000_000)) * Decimal(
            "7.0"
        )

    async def run_provider(
        self,
        text: str,
        gpt_model: str,
        user_id: int,
        message: types.Message = None,
        bot: Bot = None,
    ) -> Optional[tuple[str, list]]:
        """Runs the appropriate GPT provider based on the selected model.

        Args:
            text (str): The text to send to the GPT provider.
            gpt_model (str): The name of the GPT model to use.
            user_id (int): The ID of the user making the request.
            message (types.Message, optional): The message object. Defaults to None.
            bot (Bot, optional): The bot object. Defaults to None.

        Returns:
            Optional[tuple[str, list]]: A tuple containing the response text and a list of image URLs, or None if the model is not supported.
        """
        if gpt_model == GPTModel.GPT_FOR_INTERNET.value:
            return await self.get_morphic_answer(text)
        elif gpt_model == GPTModel.GEMINI.value:
            response = await self.get_gemini_answer(
                text, user_id, message, bot
            )
            return response, []
        return None


# --- Initialize Cache and Router ---
message_history_cache = TTLCache(
    maxsize=CACHE_MAXSIZE, ttl=CACHE_TTL
)

# --- Configure Gemini ---
genai.configure(api_key=TOKEN_GEMINI)

# --- Initialize GPT Service ---
gpt_service = GPTService()


# --- Helper Functions ---
async def update_model_choice(
    callback_query: types.CallbackQuery, model: GPTModel, bot: Bot
):
    """Updates the user's choice of GPT model in the database.

    Args:
        callback_query (types.CallbackQuery): The callback query object.
        model (GPTModel): The selected GPT model.
        bot (Bot): The bot object.
    """
    async with create_db_connection() as conn:
        await conn.execute(
            "UPDATE chats SET gpt_model = $1 WHERE user_id = $2",
            model.value,
            callback_query.from_user.id,
        )

    def update_keyboard_markup(markup):
        for i, row in enumerate(markup.inline_keyboard):
            for j, button in enumerate(row):
                if button.text.startswith(model.display_name):
                    markup.inline_keyboard[i][j].text = (
                        "✅ " + button.text.lstrip("✅ ")
                    )
                else:
                    markup.inline_keyboard[i][j].text = button.text.lstrip(
                        "✅ "
                    )
        return markup

    try:
        await bot.edit_message_reply_markup(
            chat_id=callback_query.message.chat.id,
            message_id=callback_query.message.message_id,
            reply_markup=update_keyboard_markup(
                callback_query.message.reply_markup
            ),
        )
    except Exception as e:
        print(f"Error editing message reply markup: {e}")
    finally:
        await callback_query.answer()


async def process_gpt_request(
    message: Message,
    bot: Bot,
    gpt_model: str,
    text: str,
    user_id: int,
    mes_id: int
):
    """Processes a GPT request and sends the response to the user.

    Args:
        message (Message): The message object.
        bot (Bot): The bot object.
        gpt_model (str): The name of the GPT model to use.
        text (str): The text to send to the GPT provider.
        user_id (int): The ID of the user making the request.
    """
    (
        responses,
        image_urls,
    ) = await gpt_service.run_provider(
        text, gpt_model, user_id, message, bot
    )

    if responses:
        parts = [
            responses[i : i + RESPONSE_CHUNK_SIZE]
            for i in range(0, len(responses), RESPONSE_CHUNK_SIZE)
        ]

        for part in parts:
            try:
                await bot.delete_message(
                    chat_id=message.chat.id, message_id=mes_id.message_id
                )
            except Exception as ex:
                print("GPT_response| bot.delete_message:", ex)

            keyboard = InlineKeyboardBuilder()
            keyboard.row(
                types.InlineKeyboardButton(
                    text="🔄 regenerate", callback_data="gpt_restart"
                )
            )
            if gpt_model == GPTModel.GEMINI.value:
                keyboard.add(
                    types.InlineKeyboardButton(
                        text="🧹clean history",
                        callback_data="gpt_reset_history",
                    )
                )

            if (
                len(part) > RESPONSE_TRIM_LENGTH
                and gpt_model != GPTModel.GEMINI.value
            ):
                part = f"<blockquote expandable>{part}</blockquote>"
            part += f"\n\n<i>{gpt_model.upper()}</i>"

            try:
                await message.reply(
                    text=part,
                    reply_markup=keyboard.as_markup(),
                    link_preview_options=types.link_preview_options.LinkPreviewOptions(
                        show_above_text=True
                    ),
                )
            except Exception as ex:
                print(f"Error sending message: {ex}")
                await message.reply(
                    text=part,
                    reply_markup=keyboard.as_markup(),
                    link_preview_options=types.link_preview_options.LinkPreviewOptions(
                        show_above_text=True
                    ),
                    parse_mode=None,
                )

        if image_urls:
            media = []
            for url in image_urls:
                try:
                    async with aiohttp.ClientSession() as session:
                        async with session.get(url) as response:
                            response.raise_for_status()
                            photo_bytes = await response.read()
                    photo_file = BufferedInputFile(
                        photo_bytes, filename="photo.jpg"
                    )
                    media.append(InputMediaPhoto(media=photo_file))
                except aiohttp.ClientError as e:
                    print(f"Error downloading image: {e}")
                    media.append(InputMediaPhoto(media=url))
            if media:
                try:
                    await message.reply_media_group(media)
                except Exception as e:
                    print(f"Error sending media group: {e}")
    else:
        try:
            await bot.delete_message(
                chat_id=message.chat.id, message_id=mes_id.message_id
            )
        except Exception as ex:
            print("GPT_response| bot.delete_message:", ex)
        await message.reply(
            text="Sorry, this model is not supported. Please choose another model."
        )


async def show_model_selection(
    message: Message, gpt_model: Optional[str] = None
):
    """Shows the model selection keyboard to the user.

    Args:
        message (Message): The message object.
        gpt_model (Optional[str], optional): The currently selected GPT model. Defaults to None.
    """
    keyboard = InlineKeyboardBuilder()
    buttons: List[InlineKeyboardButton] = []

    for model in GPTModel:
        button_text = (
            f"✅ {model.display_name}"
            if gpt_model == model.value
            else model.display_name
        )
        buttons.append(
            types.InlineKeyboardButton(
                text=button_text, callback_data=model.value
            )
        )

    keyboard.row(*buttons, width=1)
    await message.reply(
        text="<b>Choose the GPT Model</b>",
        reply_markup=keyboard.as_markup(),
    )


async def check_access(conn: any, bot: Bot, message: types.Message, gpt_model: str) -> bool:
    """Checks user access to Gemini AI.

    Returns:
        True, if access is granted, otherwise False.
    """

    if gpt_model != GPTModel.GEMINI.value:
        return True  # Access is granted for other models

    user_id = message.from_user.id

    user_data = await conn.fetchrow("SELECT has_access FROM users_token WHERE user_id = $1", user_id)

    if user_data:
        has_access = user_data['has_access']
        if not has_access:
            # Check subscription and group only if has_access is False
            if not await check_subscription(bot, user_id):
                channel_info = await bot.get_chat(CHANNEL_ID)
                await message.reply(f"Please subscribe to our channel @{channel_info.username} to use Gemini AI.")
                return False

            # if message.chat.id != GROUP_ID:
            #     channel_info = await bot.get_chat(CHANNEL_ID)
            #     await message.reply(f"Please use the bot in the channel group @{channel_info.username} to use Gemini AI.")
            #     return False
            # else:
            #     return True # Allow access if in the correct group and subscribed
    else:
        # If the user is not in the database, add them with access
        await conn.execute("INSERT INTO users_token (user_id, has_access) VALUES ($1, TRUE)", user_id)

    return True  # Access is granted


# --- Message Handlers ---
@router_GPT.message(
    Command("gpt"),
    F.voice
    | F.audio
    | F.photo
    | F.document,
)
async def handle_gpt_media(
    message: types.Message, bot: Bot, command: CommandObject
):
    """Handles GPT requests for media content.

    Args:
        message (types.Message): The message object.
        bot (Bot): The bot object.
        command (CommandObject): The command object.
    """
    await GPT_response(message, bot, command)


@router_GPT.message(
    (F.quote.text) & (F.reply_to_message.from_user.username == "earntestbot")
)
async def handle_gpt_quote(
    message: types.Message, bot: Bot
):
    """Handles GPT requests for media content.

    Args:
        message (types.Message): The message object.
        bot (Bot): The bot object.
        command (CommandObject): The command object.
    """
    await GPT_response(message, bot)


@router_GPT.message(Command("gpt"))
async def GPT_response(message: Message, bot: Bot, command: CommandObject = None):
    """Handles GPT requests from the user."""
    user_id = message.from_user.id
    text = command.args if command and command.args else message.quote.text if message.quote else None

    async with create_db_connection() as conn:
        gpt_model = await conn.fetchval("SELECT gpt_model FROM chats WHERE user_id = $1", user_id)
        if not gpt_model or gpt_model not in [model.value for model in GPTModel] or not text:
            return await show_model_selection(message, gpt_model)

        if not await check_access(conn, bot, message, gpt_model):
            return

    mes_id = await message.reply(text="<i>⏳ Please, wait...</i>")
    context = {}

    youtube_link = extract_video_id(text)
    web_page = text and (text.startswith("http://") or text.startswith("https://"))

    if youtube_link:
        parts = text.split(maxsplit=1)
        transcript_file = io.StringIO(get_youtube_transcript(youtube_link))
        context["youtube_transcript_file"] = transcript_file
        if len(parts) > 1:
            context["question"] = parts[1].strip()

    elif message.quote:
        context["quoted_text"] = message.quote.text
        context["question"] = message.text
    elif web_page and not youtube_link:
        try:
            parts = text.split(maxsplit=1)
            context["webpage_content"] = await get_text_content_with_playwright(parts[0].strip())
        except Exception as e:
            print(f"Ошибка при получении текста страницы: {e}")
            return await bot.edit_message_text(text="Error: Не удалось получить текст страницы.", chat_id=message.chat.id, message_id=mes_id.message_id)
    else:
        context["user_text"] = text

    if not any(context.values()) or (youtube_link and not context["youtube_transcript_file"]):
        return await bot.edit_message_text(text="Error", chat_id=message.chat.id, message_id=mes_id.message_id)

    await process_gpt_request(message, bot, gpt_model, context, user_id, mes_id)


@router_GPT.callback_query(
    lambda c: c.data in [model.value for model in GPTModel]
)
async def on_gpt_model_chosen(
    callback_query: types.CallbackQuery, bot: Bot
):
    """Handles the selection of a GPT model.

    Args:
        callback_query (types.CallbackQuery): The callback query object.
        bot (Bot): The bot object.
    """
    model = GPTModel(callback_query.data)
    await update_model_choice(callback_query, model, bot)


@router_GPT.callback_query(F.data == "gpt_restart")
async def process_gpt_restart(
    callback_query: types.CallbackQuery, bot: Bot
):
    """Processes the regeneration of a GPT response.

    Args:
        callback_query (types.CallbackQuery): The callback query object.
        bot (Bot): The bot object.
    """
    user_id = callback_query.from_user.id
    if user_id in message_history_cache and message_history_cache[user_id]:
        message_history_cache[user_id].pop(-1)

    async with create_db_connection() as conn:
        gpt_model = await conn.fetchval(
            "SELECT gpt_model FROM chats WHERE user_id = $1",
            callback_query.from_user.id,
        )

    await bot.edit_message_text(
        text="<i>⏳ Please, wait...</i>",
        chat_id=callback_query.message.chat.id,
        message_id=callback_query.message.message_id,
    )

    text = callback_query.message.reply_to_message.text
    if text:
        text = text.replace("/gpt ", "")
    else:
        await bot.edit_message_text(
            text="Error: No text to regenerate.",
            chat_id=callback_query.message.chat.id,
            message_id=callback_query.message.message_id,
        )
        return

    (
        response,
        image_urls,
    ) = await gpt_service.run_provider(
        text, gpt_model, callback_query.from_user.id,
    )

    if response:
        if (
            len(response) > RESPONSE_TRIM_LENGTH
            and gpt_model != GPTModel.GEMINI.value
        ):
            response = f"<blockquote expandable>{response}</blockquote>"
        response += f"\n\n<i>{gpt_model.upper()}</i>"

        if (
            response.replace("</i>", "").replace("<i>", "")
            != callback_query.message.text
        ):
            await bot.edit_message_text(
                text=response,
                chat_id=callback_query.message.chat.id,
                message_id=callback_query.message.message_id,
                reply_markup=callback_query.message.reply_markup,
                link_preview_options=types.link_preview_options.LinkPreviewOptions(
                    show_above_text=True
                ),
            )

        if image_urls:
            media = []
            for url in image_urls:
                try:
                    async with aiohttp.ClientSession() as session:
                        async with session.get(url) as resp:
                            resp.raise_for_status()
                            photo_bytes = await resp.read()
                    photo_file = BufferedInputFile(
                        photo_bytes, filename="photo.jpg"
                    )
                    media.append(InputMediaPhoto(media=photo_file))
                except aiohttp.ClientError as e:
                    print(f"Error downloading image: {e}")
                    media.append(InputMediaPhoto(media=url))
            if media:
                try:
                    await callback_query.message.reply_media_group(media)
                except Exception as e:
                    print(f"Error sending media group: {e}")


@router_GPT.callback_query(F.data == "gpt_reset_history")
async def reset_gpt_history(
    callback_query: types.CallbackQuery, bot: Bot
):
    """Resets the GPT history for the user.

    Args:
        callback_query (types.CallbackQuery): The callback query object.
        bot (Bot): The bot object.
    """
    user_id = callback_query.from_user.id
    if user_id in message_history_cache:
        del message_history_cache[user_id]

    entities = (
        [
            types.MessageEntity(
                type=e.type,
                offset=e.offset,
                length=e.length,
                url=e.url,
                user=e.user,
                language=e.language,
                custom_emoji_id=e.custom_emoji_id,
            )
            for e in callback_query.message.entities
        ]
        if callback_query.message.entities
        else None
    )

    await bot.edit_message_text(
        callback_query.message.text
        + "\n---------------\n🧹 History has been cleared.",
        chat_id=callback_query.message.chat.id,
        message_id=callback_query.message.message_id,
        entities=entities,
        parse_mode=None,
    )


@router_GPT.message(Command("si"))
async def set_system_instruction(
    message: Message, command: CommandObject = None
):
    """Sets the system instruction for the user.

    Args:
        message (Message): The message object.
        command (CommandObject, optional): The command object. Defaults to None.
    """
    user_id = message.from_user.id
    new_instruction = (
        command.args if command and command.args else None
    )

    async with create_db_connection() as conn:
        if new_instruction:
            new_instruction = new_instruction[
                :MAX_SYSTEM_INSTRUCTION_LENGTH
            ]
            await conn.execute(
                "UPDATE chats SET system_instruction = $1 WHERE user_id = $2",
                new_instruction,
                user_id,
            )
            await message.reply(
                f"System instruction updated:\n{new_instruction}"
            )
        else:
            await conn.execute(
                "UPDATE chats SET system_instruction = NULL WHERE user_id = $1",
                user_id,
            )
            await message.reply("System instruction cleared.")


@router_GPT.message(OwnerList([1431465053, 1147422378]), Command('add_gemini'))
async def add_gemini_user(message: Message, command: CommandObject = None):
    user_id = await get_user_id_from_command(message, command)
    if not user_id:
        return

    async with create_db_connection() as conn:
        existing_user = await conn.fetchrow("SELECT 1 FROM users_token WHERE user_id = $1", user_id)

        if existing_user:
            await conn.execute("UPDATE users_token SET has_access = TRUE WHERE user_id = $1", user_id)
            await message.reply(f"User with ID {user_id} granted access to Gemini (unfrozen).")
        else:
            await conn.execute("INSERT INTO users_token (user_id, has_access) VALUES ($1, TRUE)", user_id)
            await message.reply(f"User with ID {user_id} added to Gemini access list with granted access.")


@router_GPT.message(OwnerList([1431465053, 1147422378]), Command('del_gemini'))
async def delete_gemini_user(message: Message, command: CommandObject = None):
    user_id = await get_user_id_from_command(message, command)
    if not user_id:
        return

    async with create_db_connection() as conn:
        existing_user = await conn.fetchrow("SELECT 1 FROM users_token WHERE user_id = $1", user_id)

        if existing_user:
            await conn.execute("UPDATE users_token SET has_access = FALSE WHERE user_id = $1", user_id)
            await message.reply(f"User with ID {user_id} removed from Gemini access list (frozen).")
        else:
            await message.reply(f"User with ID {user_id} not found in Gemini access list.")


async def get_user_id_from_command(message: Message, command: CommandObject = None):
    if message.reply_to_message:
        return message.reply_to_message.from_user.id
    elif command.args:
        try:
            return int(command.args)
        except ValueError:
            await message.reply("Invalid user ID format.")
            return None
    else:
        await message.reply("Please, provide user ID: /add_gemini <user_id> or reply to user's message.", parse_mode=None)
        return None