diff --git a/SenkoGuardian/SenModules/Gemini.py b/SenkoGuardian/SenModules/Gemini.py index 4fa283a..2117455 100644 --- a/SenkoGuardian/SenModules/Gemini.py +++ b/SenkoGuardian/SenModules/Gemini.py @@ -3,9 +3,7 @@ # This software is released under the MIT License. # https://opensource.org/licenses/MIT -__version__ = (5, 7, 0) #перепешите на меня квартиру пж - -#ладно +__version__ = (5, 8, 1) #фыр # meta developer: @SenkoGuardianModules @@ -21,10 +19,14 @@ import os import io import random import socket +import base64 +import uuid +import json +from PIL import Image import asyncio import logging import tempfile -import httpx +import aiohttp from datetime import datetime from markdown_it import MarkdownIt import pytz @@ -59,11 +61,13 @@ DB_GAUTO_HISTORY_KEY = "gemini_gauto_conversations_v1" DB_IMPERSONATION_KEY = "gemini_impersonation_chats" GEMINI_TIMEOUT = 840 MAX_FFMPEG_SIZE = 90 * 1024 * 1024 +DB_KEY_MAP_KEY = "gemini_key_model_map" +CHECK_MODEL = "gemini-2.5-pro" # requires: google-genai google-api-core pytz markdown_it_py class Gemini(loader.Module): - """Модуль для работы с Google Gemini AI (New SDK). Поддержка видео/фото/аудио и контекста пользователей.""" + """Модуль для работы с Google Gemini AI. (Поддержка видео/фото/аудио""" strings = { "name": "Gemini", "cfg_api_key_doc": "API ключи Google Gemini, разделенные запятой. Будут скрыты.", @@ -78,6 +82,8 @@ class Gemini(loader.Module): "cfg_impersonation_reply_chance_doc": "Вероятность ответа в режиме gauto (от 0.0 до 1.0). 0.2 = 20% шанс.", "cfg_temperature_doc": "Температура генерации (креативность). От 0.0 до 2.0. По умолчанию 1.0.", "cfg_google_search_doc": "Включить поиск Google (Grounding) для актуальной информации.", + "cfg_image_model_doc": "Модель Gemini для генерации изображений (например: gemini-2.5-flash-image).", + "cfg_inline_pagination_doc": "Использовать инлайн-кнопки для длинных ответов.", "no_api_key": '❗️ Api ключ(и) не настроен(ы).\nПолучить Api ключ можно здесь.\nДобавьте ключ(и) в конфиге модуля: .cfg gemini api_key', "invalid_api_key": '❗️ Предоставленный API ключ недействителен.\nУбедитесь, что он правильно скопирован из Google AI Studio и что для него включен Gemini API.', "all_keys_exhausted": "❗️ Все доступные API ключи ({}) исчерпали свою квоту.\nПопробуйте позже или добавьте новые ключи в конфиге: .cfg gemini api_key", @@ -135,7 +141,7 @@ class Gemini(loader.Module): "gme_sent_to_saved": "💾 История экспортирована в избранное.", "new_sdk_missing": "⚠️ Для работы модуля нужна библиотека google-genai.\nВыполните: pip install google-genai", "gprompt_usage": "ℹ️ Использование:\n.gprompt <текст> — установить промпт.\n.gprompt -c — очистить.\nИли ответьте на .txt файл.", - "gprompt_updated": "✅ Системный промпт обновлен!\nДлина: {} симв.", + "gprompt_updated": "✅ Системный промпт обновлен!\nДлина: {} символов.", "gprompt_cleared": "🗑 Системный промпт очищен.", "gprompt_current": "📝 Текущий системный промпт:", "gprompt_file_error": "❗️ Ошибка чтения файла: {}", @@ -143,6 +149,7 @@ class Gemini(loader.Module): "gprompt_not_text": "❗️ Это не похоже на текстовый файл.(txt)", "gmodel_no_models": "⚠️ Не удалось получить список моделей.", "gmodel_list_error": "❗️ Ошибка получения списка: {}", + "gimg_process": " Генерация...\n🧠 Модель: {model}", } TEXT_MIME_TYPES = { "text/plain", "text/markdown", "text/html", "text/css", "text/csv", @@ -174,6 +181,8 @@ class Gemini(loader.Module): loader.ConfigValue("gauto_in_pm", False, "Разрешить авто-ответы в личных сообщениях (ЛС).", validator=loader.validators.Boolean()), loader.ConfigValue("google_search", False, self.strings["cfg_google_search_doc"], validator=loader.validators.Boolean()), loader.ConfigValue("temperature", 1.0, self.strings["cfg_temperature_doc"], validator=loader.validators.Float(minimum=0.0, maximum=2.0)), + loader.ConfigValue("inline_pagination", False, self.strings["cfg_inline_pagination_doc"], validator=loader.validators.Boolean()), + loader.ConfigValue("image_model_name", "gemini-2.5-flash-image", self.strings["cfg_image_model_doc"]), ) self.conversations = {} self.gauto_conversations = {} @@ -181,16 +190,25 @@ class Gemini(loader.Module): self.impersonation_chats = set() self._lock = asyncio.Lock() self.memory_disabled_chats = set() + self.pager_cache = {} + self.key_model_map = {} + self.prompt_presets = [] + self.api_keys = [] async def client_ready(self, client, db): self.client = client self.db = db self.me = await client.get_me() + api_key_str = self.config["api_key"] + self.api_keys = [k.strip() for k in api_key_str.split(",") if k.strip()] if api_key_str else [] + self.key_model_map = self.db.get(self.strings["name"], DB_KEY_MAP_KEY, {}) + keys_to_remove = [k for k in self.key_model_map if k not in self.api_keys] + if keys_to_remove: + for k in keys_to_remove: del self.key_model_map[k] + self.db.set(self.strings["name"], DB_KEY_MAP_KEY, self.key_model_map) if not GOOGLE_AVAILABLE: logger.error("Gemini: 'google-genai' library missing! pip install google-genai") return - api_key_str = self.config["api_key"] - self.api_keys = [k.strip() for k in api_key_str.split(",") if k.strip()] if api_key_str else [] self.current_api_key_index = 0 self.conversations = self._load_history_from_db(DB_HISTORY_KEY) self.gauto_conversations = self._load_history_from_db(DB_GAUTO_HISTORY_KEY) @@ -332,16 +350,13 @@ class Gemini(loader.Module): raw_hist = self._get_structured_history(chat_id, gauto=impersonation_mode) if regeneration and raw_hist: raw_hist = raw_hist[:-2] for item in raw_hist: - contents.append(types.Content( - role=item['role'], - parts=[types.Part(text=item['content'])] - )) + contents.append(types.Content(role=item['role'], parts=[types.Part(text=item['content'])])) request_parts = list(current_turn_parts) if not impersonation_mode: try: user_timezone = pytz.timezone(self.config["timezone"]) except pytz.UnknownTimeZoneError: user_timezone = pytz.utc now = datetime.now(user_timezone) - time_note = f"[System note: Current time is {now.strftime('%Y-%m-%d %H:%M:%S %Z')}]" + time_note = f"[System Info: Current local time is {now.strftime('%Y-%m-%d %H:%M:%S %Z')}]" if request_parts and getattr(request_parts[0], 'text', None): request_parts[0] = types.Part(text=f"{time_note}\n\n{request_parts[0].text}") else: @@ -367,22 +382,19 @@ class Gemini(loader.Module): http_opts = None if proxy_config: http_opts = types.HttpOptions(async_client_args={"proxies": proxy_config}) - client = genai.Client(api_key=api_key, http_options=http_opts) response = await client.aio.models.generate_content( model=self.config["model_name"], contents=contents, config=gen_config ) - if response.text: result_text = response.text was_successful = True if self.config["google_search"]: search_icon = " 🌐" self.current_api_key_index = current_idx break - else: - raise ValueError("Empty response (Safety?)") + else: raise ValueError("Empty response") except Exception as e: err_str = str(e).lower() if "quota" in err_str or "exhausted" in err_str or "429" in err_str: @@ -392,8 +404,7 @@ class Gemini(loader.Module): last_error = e break try: - if not was_successful: - raise last_error or RuntimeError("Unknown generation error") + if not was_successful: raise last_error or RuntimeError("Unknown generation error") if self._is_memory_enabled(str(chat_id)): self._update_history(chat_id, current_turn_parts, result_text, regeneration, msg_obj, gauto=impersonation_mode) if impersonation_mode: return result_text @@ -406,14 +417,25 @@ class Gemini(loader.Module): question_html = f"
{utils.escape_html(request_text_for_display[:200])}
" text_to_send = f"{mem_ind}\n\n{self.strings['question_prefix']}\n{question_html}\n\n{self.strings['response_prefix']}{search_icon}\n{formatted_body}" buttons = self._get_inline_buttons(chat_id, base_message_id) if self.config["interactive_buttons"] else None - if len(text_to_send) > 4096: + is_long_text = len(result_text) > 3500 + if is_long_text and self.config["inline_pagination"]: + chunks = self._paginate_text(result_text, 3000) + uid = uuid.uuid4().hex[:6] + header = f"{mem_ind}\n\n{self.strings['question_prefix']}
{utils.escape_html(request_text_for_display[:100])}...
\n\n{self.strings['response_prefix']}{search_icon}\n" + self.pager_cache[uid] = { + "chunks": chunks, + "total": len(chunks), + "header": header, + "chat_id": chat_id, + "msg_id": base_message_id + } + await self._render_page(uid, 0, call or status_msg) + elif len(text_to_send) > 4096: file_content = (f"Вопрос: {display_prompt}\n\n════════════════════\n\nОтвет Gemini:\n{result_text}") - file = io.BytesIO(file_content.encode("utf-8")) - file.name = "Gemini_response.txt" + file = io.BytesIO(file_content.encode("utf-8")); file.name = "Gemini_response.txt" if call: await call.answer("Ответ длинный, отправляю файлом...", show_alert=False) await self.client.send_file(call.chat_id, file, caption=self.strings["response_too_long"], reply_to=call.message_id) - await call.edit(f"✅ {self.strings['response_too_long']}", reply_markup=None) elif status_msg: await status_msg.delete() await self.client.send_file(chat_id, file, caption=self.strings["response_too_long"], reply_to=base_message_id) @@ -451,6 +473,78 @@ class Gemini(loader.Module): use_url_context=use_url_context, display_prompt=clean_args or None ) + @loader.command() + async def gimg(self, message: Message): + """<промпт> [реплай на фото] — Генерация/Редактирование изображений через Gemini.""" + args = utils.get_args_raw(message) + reply = await message.get_reply_message() + input_bytes = None + if reply: + if reply.photo: + input_bytes = await self.client.download_media(reply, bytes) + elif reply.document and reply.document.mime_type.startswith("image/"): + input_bytes = await self.client.download_media(reply, bytes) + if not args and not input_bytes: + return await utils.answer(message, "🎨 Введите промпт.\nПример: .gimg кот в космосе") + prompt = args if args else "Describe/Modify this image" + model = self.config["image_model_name"] + m = await utils.answer(message, self.strings["gimg_process"].format(model=model)) + try: + res = await self._call_google_rest(model, prompt, input_bytes) + if "error" in res: + err_msg = res["error"]["message"] + try: err_msg = json.loads(err_msg)["error"]["message"] + except: pass + raise ValueError(err_msg) + + img_bytes = None + try: + parts = res["candidates"][0]["content"]["parts"] + for part in parts: + if "inlineData" in part: + img_bytes = base64.b64decode(part["inlineData"]["data"]) + break + except Exception as e: + raise ValueError(f"Ошибка парсинга ответа: {e}") + if not img_bytes: + raise ValueError("Модель не вернула изображение (возможно, сработал Safety Filter).") + out = io.BytesIO(img_bytes) + out.name = f"gemini_{uuid.uuid4().hex[:6]}.jpg" + await self.client.send_file( + utils.get_chat_id(message), + out, + caption=f"🎨 Gemini Image\n🧠 {model}\n📜 {utils.escape_html(prompt[:100])}", + reply_to=message.id + ) + await m.delete() + except Exception as e: + await utils.answer(m, f"❌ Ошибка:\n{utils.escape_html(str(e))}") + + @loader.command() + async def gskey(self, message: Message): + """[-h] — Сканировать ключи. -h: показать статус из кеша без проверки.""" + args = utils.get_args_raw(message).strip() + if args in ["-h", "--having", "having"]: + premium = sum(1 for v in self.key_model_map.values() if v == 1) + free = sum(1 for v in self.key_model_map.values() if v == 0) + report = ( + f"📊 Статус ключей (кеш):\n" + f"💎 Premium/Active: {premium}\n" + f"👻 Free/Unknown: {free}\n" + f"🔑 Всего в конфиге: {len(self.api_keys)}" + ) + return await utils.answer(message, report) + await utils.answer(message, "⌛️ Сканирую ключи...\nЭто займет время (1.2 сек на ключ).") + report, invalid_keys = await self._scan_keys(force=True) + if invalid_keys: + txt_keys = "\n".join(invalid_keys) + try: + await self.client.send_message("me", f"🚫 Gemini: Найдены невалидные ключи:\nУдали их из конфига:\n\n{txt_keys}") + report += "\n\n⚠️ Список невалидных ключей отправлен в Избранное." + except: + report += "\n\n⚠️ Найдены невалидные ключи." + await utils.answer(message, report) + @loader.command() async def gch(self, message: Message): """<[id чата]> <кол-во> <вопрос> - Проанализировать историю чата.""" @@ -479,6 +573,8 @@ class Gemini(loader.Module): entity = await self.client.get_entity(target_chat_id) chat_name = utils.escape_html(get_display_name(entity)) chat_log = await self._get_recent_chat_text(target_chat_id, count=count, skip_last=False) + except (ValueError, TypeError, ChatAdminRequiredError, UserNotParticipantError, ChannelPrivateError) as e: + return await utils.answer(status_msg, self.strings["gch_chat_error"].format(target_chat_id, e.__class__.__name__)) except Exception as e: return await utils.answer(status_msg, self.strings["gch_chat_error"].format(target_chat_id, e)) full_prompt = ( @@ -671,7 +767,6 @@ class Gemini(loader.Module): import json hist = json.loads(f) if not isinstance(hist, list): raise ValueError - cid = utils.get_chat_id(message) target = self.gauto_conversations if gauto else self.conversations target[str(cid)] = hist @@ -681,7 +776,7 @@ class Gemini(loader.Module): @loader.command() async def gmemfind(self, message: Message): - """[слово] — Поиск по истории текущего чата по ключевому слову или фразе.""" + """[слово] — Поиск в памяти текущего чата по ключевому слову или фразе.""" q = utils.get_args_raw(message).lower() if not q: return await utils.answer(message, "Укажите слово для поиска.") cid = utils.get_chat_id(message) @@ -755,6 +850,118 @@ class Gemini(loader.Module): self._save_history_sync(False) await utils.answer(message, self.strings["memory_fully_cleared"].format(n)) + @loader.callback_handler() + async def gemini_callback_handler(self, call: InlineCall): + if not call.data.startswith("gemini:"): return + parts = call.data.split(":") + action = parts[1] + if action == "noop": + await call.answer() + return + if action == "pg": + uid = parts[2] + page = int(parts[3]) + await self._render_page(uid, page, call) + return + + async def _clear_callback(self, call: InlineCall, cid): + self._clear_history(cid, gauto=False) + await call.edit(self.strings["memory_cleared"], reply_markup=None) + + async def _regenerate_callback(self, call: InlineCall, mid, cid): + key = f"{cid}:{mid}" + if key not in self.last_requests: return await call.answer(self.strings["no_last_request"], show_alert=True) + parts, disp = self.last_requests[key] + use_url_context = bool(re.search(r'https?://\S+', disp or "")) + await self._send_to_gemini(mid, parts, regeneration=True, call=call, chat_id_override=cid, display_prompt=disp, use_url_context=use_url_context) + + async def _close_callback(self, call: InlineCall, uid: str): + """Обрабатывает нажатие кнопки закрытия для пагинации""" + await call.answer() + if uid in self.pager_cache: + del self.pager_cache[uid] + try: + await self.client.delete_messages(call.chat_id, call.message_id) + except Exception: + try: + await call.edit("✔️ Сессия закрыта.", reply_markup=None) + except Exception: + pass + + async def _render_page(self, uid, page_num, entity): + data = self.pager_cache.get(uid) + if not data: + if isinstance(entity, InlineCall): + await entity.edit("⚠️ Сессия истекла (RAM cleared).", reply_markup=None) + return + chunks = data["chunks"] + total = data["total"] + header = data.get("header", "") + raw_text_chunk = chunks[page_num] + safe_text = self._markdown_to_html(raw_text_chunk) + text_to_show = f"{header}
{safe_text}
" + nav_row = [] + if page_num > 0: + nav_row.append({"text": "◀️", "data": f"gemini:pg:{uid}:{page_num - 1}"}) + nav_row.append({"text": f"{page_num + 1}/{total}", "data": "gemini:noop"}) + if page_num < total - 1: + nav_row.append({"text": "▶️", "data": f"gemini:pg:{uid}:{page_num + 1}"}) + extra_row = [{"text": "❌ Закрыть", "callback": self._close_callback, "args": (uid,)}] + if data.get("chat_id") and data.get("msg_id"): + extra_row.append({"text": "🔄", "callback": self._regenerate_callback, "args": (data['msg_id'], data['chat_id'])}) + buttons = [nav_row, extra_row] + if isinstance(entity, Message): + await self.inline.form(text=text_to_show, message=entity, reply_markup=buttons) + elif isinstance(entity, InlineCall): + await entity.edit(text=text_to_show, reply_markup=buttons) + elif hasattr(entity, "edit"): + try: await entity.edit(text=text_to_show, reply_markup=buttons) + except: pass + + def _paginate_text(self, text: str, limit: int) -> list: + pages = [] + current_page_lines = [] + current_len = 0 + in_code_block = False + current_code_lang = "" + lines = text.split('\n') + for line in lines: + line_len = len(line) + 1 + stripped = line.strip() + if stripped.startswith("```"): + if in_code_block: + in_code_block = False + current_code_lang = "" + else: + in_code_block = True + current_code_lang = stripped.replace("```", "").strip() + if current_len + line_len > limit: + if current_page_lines: + if in_code_block: current_page_lines.append("```") + pages.append("\n".join(current_page_lines)) + current_page_lines = [] + current_len = 0 + if in_code_block: + header = f"```{current_code_lang}" + current_page_lines.append(header) + current_len += len(header) + 1 + if line_len > limit: + chunks = [line[i:i+limit] for i in range(0, len(line), limit)] + for chunk in chunks: + if current_len + len(chunk) > limit: + pages.append("\n".join(current_page_lines)) + current_page_lines = [chunk] + current_len = len(chunk) + else: + current_page_lines.append(chunk) + current_len += len(chunk) + continue + current_page_lines.append(line) + current_len += line_len + if current_page_lines: + pages.append("\n".join(current_page_lines)) + return pages + @loader.watcher(only_incoming=True, ignore_edited=True) async def watcher(self, message: Message): if not hasattr(message, 'chat_id'): return @@ -806,10 +1013,13 @@ class Gemini(loader.Module): user_id = self.me.id user_name = get_display_name(self.me) message_id = getattr(message, "id", None) - if message: - if message.sender_id: - user_id = message.sender_id + try: + peer_id = get_peer_id(message) + if peer_id: + user_id = peer_id + except (TypeError, ValueError): + if message.sender_id: user_id = message.sender_id if message.sender: user_name = get_display_name(message.sender) user_text = " ".join([p.text for p in user_parts if hasattr(p, "text") and p.text]) or "[ответ на медиа]" @@ -838,7 +1048,6 @@ class Gemini(loader.Module): "date": now, "user_id": None } - history.extend([user_entry, model_entry]) limit = self.config["max_history_length"] if limit > 0 and len(history) > limit * 2: @@ -853,8 +1062,6 @@ class Gemini(loader.Module): del d[str(cid)] self._save_history_sync(gauto) - def _is_memory_enabled(self, cid): return cid not in self.memory_disabled_chats - def _markdown_to_html(self, text): text = re.sub(r"^(#+)\s+(.*)", lambda m: f"{m.group(2)}", text, flags=re.M) text = re.sub(r"^([ \t]*)[-*+]\s+", r"\1• ", text, flags=re.M) @@ -911,42 +1118,34 @@ class Gemini(loader.Module): if txt.strip(): lines.append(f"{name}: {txt.strip()}") except: pass return "\n".join(reversed(lines)) - + def _handle_error(self, e: Exception) -> str: logger.exception("Gemini execution error") if isinstance(e, asyncio.TimeoutError): return self.strings["api_timeout"] + if google_exceptions and isinstance(e, google_exceptions.GoogleAPIError): + msg = str(e) + if "quota" in msg.lower() or "exceeded" in msg.lower(): + model = self.config.get("model_name", "unknown") + return ( + f"❗️ Превышен лимит Google Gemini API для модели {utils.escape_html(model)}.\n" + f"Детали ошибки:\n{utils.escape_html(msg)}" + ) + if "User location is not supported" in msg or "location is not supported" in msg: + return ( + '❗️ В данном регионе Gemini API не доступен.\n' + 'Используйте VPN или прокси.' + ) + if "API key not valid" in msg: + return self.strings["invalid_api_key"] + if "blocked" in msg.lower(): + return self.strings["blocked_error"].format(utils.escape_html(msg)) + return self.strings["api_error"].format(utils.escape_html(msg)) + if isinstance(e, (OSError, socket.timeout)): + return "❗️ Сетевая ошибка:\n{}".format(utils.escape_html(str(e))) msg = str(e) - if "quota" in msg.lower() or "exhausted" in msg.lower() or "429" in msg: - model = self.config.get("model_name", "unknown") - return ( - f"❗️ Превышен лимит Google Gemini API для модели {utils.escape_html(model)}." - "\n\nЧаще всего это происходит на бесплатном тарифе. Вы можете:\n" - "• Подождать, пока лимит сбросится (обычно раз в сутки).\n" - "• Проверить свой тарифный план в Google AI Studio.\n" - "• Узнать больше о лимитах здесь.\n\n" - f"Детали ошибки:\n{utils.escape_html(msg)}" - ) - if "location" in msg.lower() or "not supported" in msg.lower(): - return ( - '❗️ В данном регионе Gemini API не доступен.\n' - 'Скачайте VPN (для пк/тел) или поставьте прокси (платный/бесплатный).\n' - 'Или воспользуйтесь инструкцией вот тут\n' - 'А для тех у кого UserLand инструкция тут' - ) - if "key" in msg.lower() and "valid" in msg.lower(): - return self.strings["invalid_api_key"] - if "blocked" in msg.lower(): - return self.strings["blocked_error"].format(utils.escape_html(msg)) - if "500" in msg: - return ( - "❗️ Ошибка 500 от Google API.\n" - "Это значит, что формат медиа (файл или еще что то) который ты отправил, не поддерживается.\n" - "Такое случается, по такой причине:\n " - "• Если формат файла в принципе не поддерживается Gemini/Гуглом.\n " - "• Временный сбой на серверах Google. Попробуйте повторить запрос позже." - ) - return self.strings["api_error"].format(utils.escape_html(msg)) + if "quota" in msg.lower() or "429" in msg: return self.strings["all_keys_exhausted"].format(len(self.api_keys)) + return self.strings["generic_error"].format(utils.escape_html(msg)) def _markdown_to_html(self, text: str) -> str: def heading_replacer(match): @@ -1001,24 +1200,7 @@ class Gemini(loader.Module): async def _clear_callback(self, call: InlineCall, chat_id: int): self._clear_history(chat_id, gauto=False) - await call.edit(self.strings["memory_cleared"], reply_markup=None) - async def _regenerate_callback(self, call: InlineCall, original_message_id: int, chat_id: int): - key = f"{chat_id}:{original_message_id}" - last_request_tuple = self.last_requests.get(key) - if not last_request_tuple: - return await call.answer(self.strings["no_last_request"], show_alert=True) - last_parts, display_prompt = last_request_tuple - use_url_context = bool(re.search(r'https?://\S+', display_prompt or "")) - await self._send_to_gemini( - message=original_message_id, - parts=last_parts, - regeneration=True, - call=call, - chat_id_override=chat_id, - use_url_context=use_url_context, - display_prompt=display_prompt - ) async def _get_recent_chat_text(self, chat_id: int, count: int = None, skip_last: bool = False) -> str: history_limit = count or self.config["impersonation_history_limit"] @@ -1049,6 +1231,111 @@ class Gemini(loader.Module): logger.warning(f"Не удалось получить историю для авто-ответа: {e}") return "\n".join(reversed(chat_history_lines)) + async def _scan_keys(self, force=False): + """ + Сканирует ключи на валидность. + """ + if not GOOGLE_AVAILABLE: return "Library missing", [] + current_map_keys = list(self.key_model_map.keys()) + for k in current_map_keys: + if k not in self.api_keys: del self.key_model_map[k] + if not force and all(k in self.key_model_map for k in self.api_keys): + return "Loaded from cache", [] + if force: self.key_model_map = {} + proxy_config = self._get_proxy_config() + http_opts = types.HttpOptions(async_client_args={"proxies": proxy_config, "timeout": 10.0}) if proxy_config else None + active_keys = [] + invalid_keys = [] + minimal_config = types.GenerateContentConfig( + response_mime_type="text/plain", + max_output_tokens=1, + candidate_count=1, + safety_settings=[types.SafetySetting(category="HARM_CATEGORY_HARASSMENT", threshold="BLOCK_NONE")] + ) + for i, key in enumerate(self.api_keys): + if i > 0: await asyncio.sleep(1.2) + try: + client = genai.Client(api_key=key, http_options=http_opts) + response = await client.aio.models.generate_content( + model=CHECK_MODEL, contents="test", config=minimal_config + ) + active_keys.append(key) + self.key_model_map[key] = 1 + except Exception as e: + err = str(e).lower() + if "invalid_argument" in err or "api_key_invalid" in err or "400" in err or "blocked" in err: + invalid_keys.append(key) + else: + self.key_model_map[key] = 0 + self.db.set(self.strings["name"], DB_KEY_MAP_KEY, self.key_model_map) + short_report = ( + f"✅ Скан завершен.\n" + f"💎 Active: {len(active_keys)}\n" + f"🗑 Invalid: {len(invalid_keys)}\n" + f"👻 RateLimited/Other: {len(self.api_keys) - len(active_keys) - len(invalid_keys)}" + ) + return short_report, invalid_keys + + def _get_sorted_keys(self): + valid_keys = [] + for key in self.api_keys: + if key not in self.key_model_map: + if not self.key_model_map: valid_keys.append((key, 0, random.random())) + continue + tier = self.key_model_map[key] + valid_keys.append((key, tier, random.random())) + valid_keys.sort(key=lambda x: (x[1], x[2])) + return [item[0] for item in valid_keys] + + async def _call_google_rest(self, model_name: str, prompt: str, input_image_bytes=None): + keys = self._get_sorted_keys() + if not keys: return {"error": {"message": "Нет доступных API ключей"}} + parts = [{"text": prompt}] + if input_image_bytes: + resized = await utils.run_sync(self._resize_image_ig, input_image_bytes) + b64_img = base64.b64encode(resized).decode('utf-8') + parts.insert(0, {"inlineData": {"mimeType": "image/jpeg", "data": b64_img}}) + payload = { + "contents": [{"parts": parts}], + "safetySettings": [ + {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"}, + {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"}, + {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"}, + {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"} + ], + "generationConfig": {"candidateCount": 1, "temperature": 1.0} + } + proxy = self.config['proxy'] if self.config['proxy'] else None + last_error = None + async with aiohttp.ClientSession() as session: + for i, api_key in enumerate(keys): + url = f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent?key={api_key}" + try: + if i > 0: await asyncio.sleep(1) + async with session.post(url, json=payload, proxy=proxy, timeout=60) as resp: + if resp.status == 200: + return await resp.json() + elif resp.status in [429, 503, 403]: + last_error = f"HTTP {resp.status}" + continue + else: + text = await resp.text() + return {"error": {"message": f"HTTP {resp.status}: {text}"}} + except Exception as e: + last_error = str(e) + continue + return {"error": {"message": f"All keys exhausted. Last error: {last_error}"}} + + def _resize_image_ig(self, img_bytes): + try: + img = Image.open(io.BytesIO(img_bytes)) + img.thumbnail((1024, 1024)) + out = io.BytesIO() + if img.mode in ("RGBA", "P"): img = img.convert("RGB") + img.save(out, format='JPEG', quality=85) + return out.getvalue() + except: return img_bytes + def _is_memory_enabled(self, chat_id: str) -> bool: return chat_id not in self.memory_disabled_chats def _disable_memory(self, chat_id: int): self.memory_disabled_chats.add(str(chat_id)) def _enable_memory(self, chat_id: int): self.memory_disabled_chats.discard(str(chat_id)) diff --git a/fiksofficial/python-modules/deviceinfo.py b/fiksofficial/python-modules/deviceinfo.py index 049307a..7c2953c 100644 --- a/fiksofficial/python-modules/deviceinfo.py +++ b/fiksofficial/python-modules/deviceinfo.py @@ -106,7 +106,7 @@ class DeviceInfo(loader.Module): self.config = loader.ModuleConfig( loader.ConfigValue( "api_base_url", - "https://mobilespecs.fiksofficial.fun", + "https://gmsarena.vercel.app/", lambda: "API Url", validator=loader.validators.String() ), diff --git a/modules.json b/modules.json index 0c355e5..db43745 100644 --- a/modules.json +++ b/modules.json @@ -1,44371 +1,81001 @@ { - "Limoka.py": { - "name": "Limoka", - "description": "Modules are now in one place with easy searching!", - "meta": { - "pic": null, - "banner": null, - "developer": "@limokanews" - }, - "commands": [ - { - "limokacmd": "[query / nothing] - Search modules [запрос / ничего] — Поиск модулей" - }, - { - "lshistorycmd": "[clear] - Show or clear search history [clear] — Показать или очистить историю поиска" - } - ], - "new_commands": [ - { - "limoka": { - "ru_doc": "[запрос / ничего] — Поиск модулей", - "en_doc": null, - "doc": "[query / nothing] - Search modules" - } - }, - { - "lshistory": { - "ru_doc": "[clear] — Показать или очистить историю поиска", - "en_doc": null, - "doc": "[clear] - Show or clear search history" - } - } - ], - "category": [ - "Tools", - "Chat" - ] - }, - "GeekTG/FTG-Modules/terminal.py": { - "name": "TerminalMod", - "description": "Runs commands", - "meta": { - "pic": null, - "banner": null - }, - "commands": [ - { - "terminalcmd": ".terminal " - }, - { - "aptcmd": "Shorthand for '.terminal apt'" - }, - { - "terminatecmd": "Use in reply to send SIGTERM to a process" - }, - { - "killcmd": "Use in reply to send SIGKILL to a process" - }, - { - "neofetchcmd": "Show system stats via neofetch" - }, - { - "uptimecmd": "Show system uptime" - } - ], - "new_commands": [ - { - "terminal": { - "ru_doc": null, - "en_doc": null, - "doc": ".terminal " - } - }, - { - "apt": { - "ru_doc": null, - "en_doc": null, - "doc": "Shorthand for '.terminal apt'" - } - }, - { - "terminate": { - "ru_doc": null, - "en_doc": null, - "doc": "Use in reply to send SIGTERM to a process" - } - }, - { - "kill": { - "ru_doc": null, - "en_doc": null, - "doc": "Use in reply to send SIGKILL to a process" - } - }, - { - "neofetch": { - "ru_doc": null, - "en_doc": null, - "doc": "Show system stats via neofetch" - } - }, - { - "uptime": { - "ru_doc": null, - "en_doc": null, - "doc": "Show system uptime" - } - } - ], - "category": [ - "Tools", - "Chat" - ] - }, - "GeekTG/FTG-Modules/tags.py": { - "name": "TagMod", - "description": "Secretly tag a user", - "meta": { - "pic": null, - "banner": null - }, - "commands": [ - { - "tagcmd": ".tag <@> ." - }, - { - "tagallcmd": ".tagall - tag all users in chat" - } - ], - "new_commands": [ - { - "tag": { - "ru_doc": null, - "en_doc": null, - "doc": ".tag <@> ." - } - }, - { - "tagall": { - "ru_doc": null, - "en_doc": null, - "doc": ".tagall - tag all users in chat" - } - } - ], - "category": [ - "Chat", - "Tools" - ] - }, - "GeekTG/FTG-Modules/lyrics.py": { - "name": "LyricsMod", - "description": "Sings songs", - "meta": { - "pic": null, - "banner": null - }, - "commands": [ - { - "lyricscmd": ".lyrics Song, Artist" - } - ], - "new_commands": [ - { - "lyrics": { - "ru_doc": null, - "en_doc": null, - "doc": ".lyrics Song, Artist" - } - } - ], - "category": [ - "Tools", - "Media" - ] - }, - "GeekTG/FTG-Modules/vizjener.py": { - "name": "VijenerMod", - "description": "Конвертация текста в шифр Виженеря и наоборот.", - "meta": { - "pic": null, - "banner": null - }, - "commands": [ - { - "tovizcmd": ".toviz {ключ} {текст}" - }, - { - "tounvizcmd": ".tounviz {ключ} {текст}" - } - ], - "new_commands": [ - { - "toviz": { - "ru_doc": null, - "en_doc": null, - "doc": ".toviz {ключ} {текст}" - } - }, - { - "tounviz": { - "ru_doc": null, - "en_doc": null, - "doc": ".tounviz {ключ} {текст}" - } - } - ], - "category": [ - "Tools", - "Chat" - ] - }, - "GeekTG/FTG-Modules/rpmod.py": { - "name": "RPMod", - "description": "Модуль RPMod + дополнение после команды.+реплика.(указывать реплику на второй строке)", - "meta": { - "pic": null, - "banner": null - }, - "commands": [ - { - "dobrpcmd": "Используй: .dobrp (команда) / (действие) / (эмодзи) чтобы добавить команду. Можно и без эмодзи(но и второго\nразделителя). Используй только одно слово в качестве команды." - }, - { - "delrpcmd": "Используй: .delrp (команда) чтобы удалить команду.\nИспользуй: .delrp all чтобы удалить все команды." - }, - { - "rpmodcmd": "Используй: .rpmod чтобы включить/выключить RP режим.\nИспользуй: .rpmod toggle чтобы сменить режим на отправку или изменение смс." - }, - { - "rplistcmd": "Используй: .rplist чтобы посмотреть список рп команд." - }, - { - "rpnickcmd": "Используй: .rpnick (ник) чтобы сменить свой ник. Если без аргументов, то вернётся ник из тг." - }, - { - "rpbackcmd": "Используй: .rpback чтобы выгрузить список своих рп команд.\nИспользуй .rpback / (список чьих то команд) / (список чьих то эмодзи) чтобы добавить себе список команд. можно без эмодзи, но первый разделитель обязателен." - }, - { - "rpblockcmd": "Используй: .rpblock чтобы добавить/удалить исключение(использовать в нужном чате).\nИспользуй: .rpblock list чтобы просмотреть чаты в исключениях.\nИспользуй .rpblock (ид) чтобы удалить чат из исключений." - } - ], - "new_commands": [ - { - "dobrp": { - "ru_doc": null, - "en_doc": null, - "doc": "Используй: .dobrp (команда) / (действие) / (эмодзи) чтобы добавить команду. Можно и без эмодзи(но и второго\nразделителя). Используй только одно слово в качестве команды." - } - }, - { - "delrp": { - "ru_doc": null, - "en_doc": null, - "doc": "Используй: .delrp (команда) чтобы удалить команду.\nИспользуй: .delrp all чтобы удалить все команды." - } - }, - { - "rpmod": { - "ru_doc": null, - "en_doc": null, - "doc": "Используй: .rpmod чтобы включить/выключить RP режим.\nИспользуй: .rpmod toggle чтобы сменить режим на отправку или изменение смс." - } - }, - { - "rplist": { - "ru_doc": null, - "en_doc": null, - "doc": "Используй: .rplist чтобы посмотреть список рп команд." - } - }, - { - "rpnick": { - "ru_doc": null, - "en_doc": null, - "doc": "Используй: .rpnick (ник) чтобы сменить свой ник. Если без аргументов, то вернётся ник из тг." - } - }, - { - "rpback": { - "ru_doc": null, - "en_doc": null, - "doc": "Используй: .rpback чтобы выгрузить список своих рп команд.\nИспользуй .rpback / (список чьих то команд) / (список чьих то эмодзи) чтобы добавить себе список команд. можно без эмодзи, но первый разделитель обязателен." - } - }, - { - "rpblock": { - "ru_doc": null, - "en_doc": null, - "doc": "Используй: .rpblock чтобы добавить/удалить исключение(использовать в нужном чате).\nИспользуй: .rpblock list чтобы просмотреть чаты в исключениях.\nИспользуй .rpblock (ид) чтобы удалить чат из исключений." - } - } - ], - "category": [ - "Chat", - "Automation" - ] - }, - "GeekTG/FTG-Modules/pmlog.py": { - "name": "PMLogMod", - "description": "Logs unwanted PMs to a channel", - "meta": { - "pic": null, - "banner": null - }, - "commands": [ - { - "logpmcmd": "Begins logging PMs" - }, - { - "unlogpmcmd": "Stops logging PMs" - } - ], - "new_commands": [ - { - "logpm": { - "ru_doc": null, - "en_doc": null, - "doc": "Begins logging PMs" - } - }, - { - "unlogpm": { - "ru_doc": null, - "en_doc": null, - "doc": "Stops logging PMs" - } - } - ], - "category": [ - "Tools", - "Chat" - ] - }, - "GeekTG/FTG-Modules/callcontrol.py": { - "name": "VGCallControllerMod", - "description": "Control group voice calls", - "meta": { - "pic": null, - "banner": null - }, - "commands": [ - { - "callstartcmd": "Start call in chat" - }, - { - "callstopcmd": "Stop call in chat" - } - ], - "new_commands": [ - { - "callstart": { - "ru_doc": null, - "en_doc": null, - "doc": "Start call in chat" - } - }, - { - "callstop": { - "ru_doc": null, - "en_doc": null, - "doc": "Stop call in chat" - } - } - ], - "category": [ - "Chat", - "Tools" - ] - }, - "GeekTG/FTG-Modules/admin_tools.py": { - "name": "AdminToolsMod", - "description": "Admin Tools", - "meta": { - "pic": null, - "banner": null - }, - "commands": [ - { - "ecpcmd": "Command .ecp changes the pic of the chat.\nUse: .ecp ." - }, - { - "promotecmd": "Command .promote for promote user to admin rights.\nUse: .promote <@ or reply> ." - }, - { - "demotecmd": "Command .demote for demote user to admin rights.\nUse: .demote <@ or reply>." - }, - { - "pincmd": "Command .pin for pin message in the chat.\nUse: .pin ." - }, - { - "unpincmd": "Command .unpin for unpin message in the chat.\nUse: .unpin." - }, - { - "kickcmd": "Command .kick for kick the user.\nUse: .kick <@ or reply>." - }, - { - "bancmd": "Command .ban for ban the user.\nUse: .ban <@ or reply>." - }, - { - "unbancmd": "Command .unban for unban the user.\nUse: .unban <@ or reply>." - }, - { - "mutecmd": "Command .mute for mute the user.\nUse: .mute <@ or reply>