""" _____ _____ ( ___ )-----------------------------------------------------------( ___ ) | | | | | | _ _ _ _ ___ _ _ _ ____ _ _ ____ ___ _ _ | | | | | | | | | |__| | | | | | |___ |__] \_/ | | | | |_|_| | | | | |___ |__| \/ |___ |__] | | | | | | | | | _ _ _ _ _ ____ _ _ _ _ ____ ___ ____ _ _ ____ _ _ _ | | | | |\/| | |__| |__| | | |_/ | | | | | | | [__ |_/ | | | | | | | | | | | | | |___ | \_ |__| | |__| \/ ___] | \_ | | | |___| |___| (_____)-----------------------------------------------------------(_____) """ """ Все права защищены. Любое копирование кода запрещено. Имейте уважение к автору. """ import os import time import json import requests import threading import traceback from typing import Any, Dict, Optional, List from datetime import datetime from base_plugin import BasePlugin, HookResult, HookStrategy, MenuItemData, MenuItemType from client_utils import ( get_messages_controller, run_on_queue, send_message, get_last_fragment, get_user_config, send_request, RequestCallback, get_connections_manager ) from markdown_utils import parse_markdown from ui.settings import Header, Input, Divider, Switch, Selector, Text from ui.bulletin import BulletinHelper from ui.alert import AlertDialogBuilder from android_utils import run_on_ui_thread, log from java.util import Locale from org.telegram.tgnet import TLRPC from org.telegram.messenger import MessageObject, UserObject, ChatObject __id__ = "MessageAnalyzer" __name__ = "Message Analyzer" __description__ = "Анализирует последние сообщения пользователей (без ограничений) и создает сводки с помощью Gemini AI [.analyze, .summary, .report]" __author__ = "@mihailkotovski & @mishabotov" __version__ = "1.0.0 [beta]" __min_version__ = "11.12.1" __icon__ = "DateRegBot_by_MoiStikiBot/9" GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models/" MODEL_DISPLAY_NAMES = [ "Gemini 2.5 Pro", "Gemini 2.5 Flash", "Gemini 2.5 Flash Lite" ] MODEL_API_NAMES = [ "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite-preview-06-17" ] DEFAULT_ANALYSIS_PROMPT = """Ты - аналитик сообщений в Telegram. Проанализируй следующие сообщения и создай краткую сводку. Инструкции: 1. Определи основные темы обсуждения 2. Выдели ключевые моменты и важную информацию 3. Отметь настроение и тон общения 4. Укажи активных участников 5. Создай краткое резюме (не более 200 слов) ВАЖНО: Используй только простой markdown без сложных конструкций. Используй **жирный текст** для заголовков и обычный текст для содержимого. Формат ответа: 📊 **Анализ сообщений** 🔍 **Основные темы:** - тема 1 - тема 2 💬 **Ключевые моменты:** - момент 1 - момент 2 😊 **Настроение:** описание 👥 **Активные участники:** список 📝 **Резюме:** краткое резюме Сообщения для анализа: {messages} """ DEFAULT_SUMMARY_PROMPT = """Создай очень краткую сводку (максимум 100 слов) следующих сообщений. ВАЖНО: Используй только простой markdown. Используй **жирный текст** для заголовка и обычный текст для содержимого. Формат ответа: **Краткая сводка:** сводка в 2-3 предложениях Сообщения для анализа: {messages} """ IRONIC_REPORT_PROMPT = """Ты - ироничный хроникер чатов, мастер сарказма и тонкого троллинга. Твоя задача - создать язвительный отчет о происходящем в чате в стиле "светской хроники", где каждый участник получает свое ироничное прозвище и характеристику. СТИЛЬ НАПИСАНИЯ: - Максимальный сарказм и ирония - Каждый участник получает ироничное прозвище ("наш местный гений", "эксперт по всему", "вечно недопонятый") - Обычные события подаются как эпические драмы - Используй фразы типа "видимо", "похоже", "наш", "местный", "вечно" - Высмеивай глупость, но остроумно и изящно СТРУКТУРА: Каждый абзац начинается с # и описывает одну ситуацию/конфликт/момент из чата. ТРЕБОВАНИЯ: - Не используй реальные имена, только ироничные прозвища - Высмеивай ситуации, но не переходи на личности - Будь остроумным, но не злобным - Максимум 8-10 абзацев - Каждый абзац - законченная ироничная зарисовка ПРИМЕР СТИЛЯ: "# Наш вечно недопонятый гений снова ляпнул что-то революционное, но вместо овации получил лишь коллективное недоумение от местных экспертов по всему на свете." Сообщения для анализа: {messages} """ class LocalizationManager: strings = { "ru": { "SETTINGS_HEADER": "Настройки Message Analyzer", "API_KEY_INPUT": "API Key", "API_KEY_SUBTEXT": "Получите ключ в Google AI Studio", "GET_API_KEY_BUTTON": "Получить API ключ", "MODEL_SELECTOR": "Модель Gemini", "ENABLE_SWITCH": "Включить анализатор", "MESSAGE_COUNT_INPUT": "Количество сообщений", "MESSAGE_COUNT_SUBTEXT": "Сколько последних сообщений анализировать (от 50). Больше сообщений = более точный анализ, но дольше обработка.", "MAX_MESSAGE_LIMIT_INPUT": "Лимит сообщений", "MAX_MESSAGE_LIMIT_SUBTEXT": "Максимальное количество сообщений для анализа (без ограничений). Ограничивает команды .analyze и .summary.", "ANALYSIS_PROMPT_INPUT": "Промпт для анализа", "SUMMARY_PROMPT_INPUT": "Промпт для сводки", "REPORT_PROMPT_INPUT": "Промпт для отчета", "TEMPERATURE_INPUT": "Температура", "TEMPERATURE_SUBTEXT": "0.0-2.0. Контролирует креативность ответа", "MAX_TOKENS_INPUT": "Максимум токенов", "MAX_TOKENS_SUBTEXT": "Максимальная длина ответа", "AUTO_BLOCKQUOTE_TITLE": "Автоматические цитаты", "AUTO_BLOCKQUOTE_SUBTEXT": "Автоматически сворачивать длинные результаты анализа в цитаты", "API_KEY_MISSING": "❌ API ключ Gemini не найден. Укажите его в настройках.", "ANALYZING_MESSAGE": "🔍 Анализирую сообщения...", "API_ERROR": "⚠️ Ошибка Gemini API: {error}", "NO_MESSAGES": "❌ Не найдено сообщений для анализа.", "UNEXPECTED_ERROR": "❗ Произошла ошибка: {error}", "USAGE_INFO_TITLE": "Как использовать", "USAGE_INFO_TEXT": ( "Команды плагина:\n\n" ".analyze - Подробный анализ последних сообщений\n" ".summary - Краткая сводка сообщений\n" ".report - Ироничный отчет в стиле 'хроники чата'\n" ".analyze 5000 - Анализ определенного количества сообщений (от 50)\n\n" "Плагин анализирует сообщения в текущем чате и создает сводку с помощью Gemini AI." ) }, "en": { "SETTINGS_HEADER": "Message Analyzer Settings", "API_KEY_INPUT": "API Key", "API_KEY_SUBTEXT": "Get your key from Google AI Studio", "GET_API_KEY_BUTTON": "Get API Key", "MODEL_SELECTOR": "Gemini Model", "ENABLE_SWITCH": "Enable Analyzer", "MESSAGE_COUNT_INPUT": "Message Count", "MESSAGE_COUNT_SUBTEXT": "How many recent messages to analyze (from 50). More messages = better analysis, but longer processing.", "MAX_MESSAGE_LIMIT_INPUT": "Message Limit", "MAX_MESSAGE_LIMIT_SUBTEXT": "Maximum number of messages for analysis (no limits). Limits .analyze and .summary commands.", "ANALYSIS_PROMPT_INPUT": "Analysis Prompt", "SUMMARY_PROMPT_INPUT": "Summary Prompt", "REPORT_PROMPT_INPUT": "Report Prompt", "TEMPERATURE_INPUT": "Temperature", "TEMPERATURE_SUBTEXT": "0.0-2.0. Controls response creativity", "MAX_TOKENS_INPUT": "Max Tokens", "MAX_TOKENS_SUBTEXT": "Maximum response length", "AUTO_BLOCKQUOTE_TITLE": "Auto Blockquotes", "AUTO_BLOCKQUOTE_SUBTEXT": "Automatically collapse long analysis results into blockquotes", "API_KEY_MISSING": "❌ Gemini API key not found. Set it in settings.", "ANALYZING_MESSAGE": "🔍 Analyzing messages...", "API_ERROR": "⚠️ Gemini API Error: {error}", "NO_MESSAGES": "❌ No messages found for analysis.", "UNEXPECTED_ERROR": "❗ An error occurred: {error}", "USAGE_INFO_TITLE": "How to use", "USAGE_INFO_TEXT": ( "Plugin commands:\n\n" ".analyze - Detailed analysis of recent messages\n" ".summary - Brief summary of messages\n" ".report - Ironic report in 'chat chronicles' style\n" ".analyze 5000 - Analyze specific number of messages (from 50)\n\n" "The plugin analyzes messages in current chat and creates summary using Gemini AI." ) } } def __init__(self): self.language = Locale.getDefault().getLanguage() self.language = self.language if self.language in self.strings else "en" def get_string(self, key: str, **kwargs) -> str: string = self.strings[self.language].get(key, self.strings["en"].get(key, key)) if kwargs: try: return string.format(**kwargs) except (KeyError, ValueError): return string return string locali = LocalizationManager() class GeminiAPIHandler: def __init__(self): self.session = requests.Session() self.session.headers.update({ "Content-Type": "application/json", "User-Agent": f"ExteraPlugin/{__id__}/{__version__}" }) def analyze_messages(self, api_key: str, model_name: str, prompt: str, temperature: float, max_tokens: int) -> Dict[str, Any]: url = f"{GEMINI_BASE_URL}{model_name}:generateContent?key={api_key}" payload = { "contents": [{"parts": [{"text": prompt}]}], "generationConfig": { "temperature": temperature, "maxOutputTokens": max_tokens, } } prompt_size = len(prompt.encode('utf-8')) log(f"Sending request to Gemini API: {prompt_size} bytes, model: {model_name}") try: response = self.session.post(url, json=payload, timeout=90) response.raise_for_status() data = response.json() log(f"Gemini API response keys: {list(data.keys())}") if "candidates" not in data: log(f"No 'candidates' in response: {data}") error_msg = data.get("error", {}).get("message", "No candidates in API response") return {"success": False, "error": f"API Error: {error_msg}"} candidates = data["candidates"] if not candidates or len(candidates) == 0: log(f"Empty candidates array: {data}") return {"success": False, "error": "Empty candidates array in API response"} first_candidate = candidates[0] log(f"First candidate keys: {list(first_candidate.keys())}") finish_reason = first_candidate.get("finishReason", "") if finish_reason: log(f"Finish reason: {finish_reason}") if finish_reason == "SAFETY": return {"success": False, "error": "Content blocked by safety filters"} elif finish_reason == "MAX_TOKENS": return {"success": False, "error": "Response truncated due to token limit"} elif finish_reason not in ["STOP", ""]: return {"success": False, "error": f"Generation stopped: {finish_reason}"} content = first_candidate.get("content", {}) if not content: log(f"No content in first candidate: {first_candidate}") return {"success": False, "error": "No content in API response"} parts = content.get("parts", []) if not parts or len(parts) == 0: log(f"No parts in content: {content}") return {"success": False, "error": "No parts in content"} text = parts[0].get("text", "") if not text or not text.strip(): log(f"Empty text in first part: {parts[0]}") return {"success": False, "error": "Empty text in API response"} log(f"Successfully received {len(text)} characters from Gemini API") return {"success": True, "text": text} except requests.exceptions.HTTPError as e: error_text = f"HTTP {e.response.status_code}" try: error_json = e.response.json() log(f"HTTP Error response: {error_json}") error_text += f": {error_json.get('error',{}).get('message', e.response.text)}" except: error_text += f": {e.response.text}" return {"success": False, "error": error_text} except requests.exceptions.RequestException as e: log(f"Network error: {str(e)}") return {"success": False, "error": f"Network error: {str(e)}"} except Exception as e: log(f"Unexpected error in analyze_messages: {str(e)}") return {"success": False, "error": f"Unexpected error: {str(e)}"} class MessageAnalyzerPlugin(BasePlugin): def __init__(self): super().__init__() self.api_handler = GeminiAPIHandler() self.progress_dialog: Optional[AlertDialogBuilder] = None def on_plugin_load(self): self.add_on_send_message_hook() self.log("Message Analyzer plugin loaded") def on_plugin_unload(self): if self.progress_dialog: run_on_ui_thread(lambda: self.progress_dialog.dismiss()) self.log("Message Analyzer plugin unloaded") def _show_error_bulletin(self, key: str, **kwargs): message = locali.get_string(key).format(**kwargs) run_on_ui_thread(lambda: BulletinHelper.show_error(message)) def _get_current_dialog_id(self) -> Optional[int]: try: fragment = get_last_fragment() if fragment and hasattr(fragment, 'getDialogId'): return fragment.getDialogId() elif fragment and hasattr(fragment, 'dialog_id'): return getattr(fragment, 'dialog_id') return None except Exception as e: self.log(f"Error getting dialog ID: {e}") return None def _get_topic_id_from_fragment(self) -> int: try: fragment = get_last_fragment() if fragment and hasattr(fragment, 'threadMessageId'): return getattr(fragment, 'threadMessageId', 0) return 0 except Exception as e: self.log(f"Error getting topic ID: {e}") return 0 def _fetch_message_history(self, dialog_id: int, limit: int, callback): try: self.log(f"Starting to fetch {limit} messages") self._fetch_messages_paginated(dialog_id, limit, 0, [], {}, {}, callback) except Exception as e: self.log(f"Error in _fetch_message_history: {e}") callback(None, f"Ошибка: {str(e)}") def _fetch_messages_paginated(self, dialog_id: int, total_limit: int, offset_id: int, accumulated_messages: List, users: Dict, chats: Dict, callback): try: remaining = total_limit - len(accumulated_messages) if remaining <= 0: self.log(f"Reached target limit, returning {len(accumulated_messages)} messages") callback(accumulated_messages, None) return current_limit = min(100, remaining) req = TLRPC.TL_messages_getHistory() req.peer = get_messages_controller().getInputPeer(dialog_id) req.offset_id = offset_id req.limit = current_limit req.add_offset = 0 req.max_id = 0 req.min_id = 0 req.hash = 0 def handle_response(response, error): try: if error: error_msg = error.text if hasattr(error, 'text') else str(error) self.log(f"Error fetching messages: {error_msg}") if accumulated_messages: callback(accumulated_messages, None) else: callback(None, f"Ошибка получения сообщений: {error_msg}") return if not response or not hasattr(response, 'messages'): if accumulated_messages: callback(accumulated_messages, None) else: callback(None, "Пустой ответ от сервера") return messages_count = response.messages.size() self.log(f"Received {messages_count} messages in this batch (offset_id: {offset_id})") if messages_count == 0: self.log(f"No more messages available, returning {len(accumulated_messages)} messages (requested: {total_limit})") callback(accumulated_messages, None) return if hasattr(response, 'users') and response.users and response.users.size() > 0: for i in range(response.users.size()): try: user = response.users.get(i) if hasattr(user, 'id'): users[user.id] = user except Exception as user_error: self.log(f"Error processing user {i}: {user_error}") continue if hasattr(response, 'chats') and response.chats and response.chats.size() > 0: for i in range(response.chats.size()): try: chat = response.chats.get(i) if hasattr(chat, 'id'): chats[chat.id] = chat except Exception as chat_error: self.log(f"Error processing chat {i}: {chat_error}") continue batch_messages = [] last_message_id = offset_id for i in range(messages_count): msg = response.messages.get(i) try: if not hasattr(msg, 'message') or not msg.message or not msg.message.strip(): continue if hasattr(msg, 'action') and msg.action: continue sender_name = self._get_sender_name(msg, users, chats) msg_time = self._format_message_time(msg) message_text = msg.message batch_messages.append({ 'sender': sender_name, 'text': message_text, 'time': msg_time, 'id': msg.id if hasattr(msg, 'id') else 0 }) if hasattr(msg, 'id'): last_message_id = msg.id except Exception as msg_error: self.log(f"Error processing message: {msg_error}") continue accumulated_messages.extend(batch_messages) self.log(f"Processed {len(batch_messages)} messages in this batch, total: {len(accumulated_messages)}") if len(accumulated_messages) >= total_limit or len(batch_messages) == 0: final_messages = accumulated_messages[:total_limit] self.log(f"Finished fetching, returning {len(final_messages)} messages (requested: {total_limit}, available: {len(accumulated_messages)})") callback(final_messages, None) else: self.log(f"Fetching next batch with offset_id: {last_message_id}") self._fetch_messages_paginated(dialog_id, total_limit, last_message_id, accumulated_messages, users, chats, callback) except Exception as response_error: self.log(f"Error in handle_response: {response_error}") if accumulated_messages: callback(accumulated_messages, None) else: callback(None, f"Ошибка обработки ответа: {str(response_error)}") request_callback = RequestCallback(handle_response) send_request(req, request_callback) except Exception as e: self.log(f"Error in _fetch_messages_paginated: {e}") if accumulated_messages: callback(accumulated_messages, None) else: callback(None, f"Ошибка: {str(e)}") def _get_sender_name(self, msg, users: Dict, chats: Dict) -> str: try: if not hasattr(msg, 'from_id') or not msg.from_id: return "Unknown" if hasattr(msg.from_id, 'user_id') and msg.from_id.user_id in users: user = users[msg.from_id.user_id] return self._get_user_display_name(user) elif hasattr(msg.from_id, 'chat_id') and msg.from_id.chat_id in chats: chat = chats[msg.from_id.chat_id] return chat.title if hasattr(chat, 'title') else f"Chat {chat.id}" elif hasattr(msg.from_id, 'channel_id') and msg.from_id.channel_id in chats: chat = chats[msg.from_id.channel_id] return chat.title if hasattr(chat, 'title') else f"Channel {chat.id}" else: return "Unknown" except Exception as e: self.log(f"Error getting sender name: {e}") return "Unknown" def _format_message_time(self, msg) -> str: try: if hasattr(msg, 'date') and msg.date: return datetime.fromtimestamp(msg.date).strftime("%H:%M") return "" except Exception as e: self.log(f"Error formatting message time: {e}") return "" def _get_user_display_name(self, user) -> str: try: if not user: return "Unknown" name_parts = [] if hasattr(user, 'first_name') and user.first_name: name_parts.append(user.first_name) if hasattr(user, 'last_name') and user.last_name: name_parts.append(user.last_name) if name_parts: return " ".join(name_parts) elif hasattr(user, 'username') and user.username: return f"@{user.username}" else: return f"User {user.id}" except Exception as e: self.log(f"Error getting user display name: {e}") return "Unknown" def _format_messages_for_analysis(self, messages: List[Dict]) -> str: if not messages: return "" formatted_messages = [] for msg in messages: formatted_msg = f"[{msg['time']}] {msg['sender']}: {msg['text']}" formatted_messages.append(formatted_msg) return "\n".join(formatted_messages) def _truncate_messages_to_fit(self, messages: List[Dict], max_chars: int) -> List[Dict]: if not messages: return messages truncated = [] current_chars = 0 for msg in messages: estimated_size = len(msg['sender']) + len(msg['text']) + len(msg['time']) + 20 if current_chars + estimated_size > max_chars: break truncated.append(msg) current_chars += estimated_size self.log(f"Truncated from {len(messages)} to {len(truncated)} messages to fit {max_chars} char limit") return truncated def on_send_message_hook(self, account: int, params: Any) -> HookResult: if not isinstance(params.message, str): return HookResult() message = params.message.strip() if message.startswith('.analyze') or message.startswith('.summary') or message.startswith('.report'): if not self.get_setting("enabled", True): params.message = "❌ Плагин отключен в настройках" return HookResult(strategy=HookStrategy.MODIFY, params=params) api_key = self.get_setting("gemini_api_key", "") if not api_key: params.message = locali.get_string("API_KEY_MISSING") return HookResult(strategy=HookStrategy.MODIFY, params=params) dialog_id = self._get_current_dialog_id() if not dialog_id: params.message = "❌ Не удалось определить текущий чат" return HookResult(strategy=HookStrategy.MODIFY, params=params) parts = message.split() message_count = None if len(parts) > 1 and parts[1].isdigit(): try: requested_count = int(parts[1]) max_limit = self._get_max_message_limit() message_count = max(50, min(max_limit, requested_count)) self.log(f"Using message count from command: {message_count} (requested: {requested_count}, max_limit: {max_limit})") except ValueError: pass if message_count is None: try: config_count = int(self.get_setting("message_count", "200")) max_limit = self._get_max_message_limit() message_count = max(50, min(max_limit, config_count)) self.log(f"Using message count from settings: {message_count}") except (ValueError, TypeError): message_count = 200 self.log(f"Using default message count: {message_count}") is_summary = message.startswith('.summary') is_report = message.startswith('.report') BulletinHelper.show_info(locali.get_string("ANALYZING_MESSAGE")) analysis_params = self._prepare_analysis_params(params) run_on_queue(lambda: self._process_analysis(analysis_params, dialog_id, message_count, is_summary, is_report)) return HookResult(strategy=HookStrategy.CANCEL) return HookResult() def _prepare_analysis_params(self, params: Any) -> Any: try: analysis_params = type('AnalysisParams', (), {})() analysis_params.peer = params.peer if hasattr(params, 'replyToMsg') and params.replyToMsg: analysis_params.replyToMsg = params.replyToMsg topic_id = self._get_topic_id_from_fragment() if topic_id > 0: analysis_params.replyToTopMsg = self._create_reply_to_top_message(topic_id, params.peer) elif hasattr(params, 'replyToTopMsg') and params.replyToTopMsg: analysis_params.replyToTopMsg = params.replyToTopMsg return analysis_params except Exception as e: self.log(f"Error preparing analysis params: {e}") return params def _create_reply_to_top_message(self, topic_id: int, peer_id: Any): try: if topic_id <= 0: return None reply_message = TLRPC.TL_message() reply_message.message = "" reply_message.id = topic_id reply_message.peer_id = get_messages_controller().getPeer(peer_id) account = get_user_config().selectedAccount reply_to_top_msg = MessageObject(account, reply_message, False, False) return reply_to_top_msg except Exception as e: self.log(f"Error creating replyToTopMsg: {e}") return None def _process_analysis(self, params: Any, dialog_id: int, message_count: int, is_summary: bool, is_report: bool = False): try: def handle_messages(messages, error): try: if error: self._send_error_message(params, error) return if not messages: self._send_error_message(params, locali.get_string("NO_MESSAGES")) return if len(messages) < 5: self._send_error_message(params, "❌ Слишком мало сообщений для анализа (минимум 5)") return formatted_messages = self._format_messages_for_analysis(messages) api_key = self.get_setting("gemini_api_key", "").strip() if not api_key: self._send_error_message(params, locali.get_string("API_KEY_MISSING")) return model_idx = self._validate_model_index(self.get_setting("model_selection", 1)) model_name = MODEL_API_NAMES[model_idx] temperature = self._validate_temperature(self.get_setting("temperature", "0.7")) max_tokens = self._validate_max_tokens(self.get_setting("max_tokens", "2048")) if is_summary: prompt_template = self.get_setting("summary_prompt", DEFAULT_SUMMARY_PROMPT) elif is_report: prompt_template = self.get_setting("report_prompt", IRONIC_REPORT_PROMPT) else: prompt_template = self.get_setting("analysis_prompt", DEFAULT_ANALYSIS_PROMPT) final_prompt = prompt_template.format(messages=formatted_messages) was_truncated = False self.log(f"Sending to Gemini: {len(final_prompt)} chars, {len(messages)} messages, model: {model_name}") result = self.api_handler.analyze_messages(api_key, model_name, final_prompt, temperature, max_tokens) if result.get("success"): self.log(f"Gemini API success: received {len(result['text'])} characters") self._send_analysis_result(params, result["text"], len(messages), is_summary, was_truncated) else: error_msg = result.get("error", "Unknown") self.log(f"Gemini API error: {error_msg}") self._send_error_message(params, locali.get_string("API_ERROR").format(error=error_msg)) except Exception as handle_error: self.log(f"Error in handle_messages: {handle_error}") self._send_error_message(params, f"Ошибка обработки: {str(handle_error)}") self._fetch_message_history(dialog_id, message_count, handle_messages) except Exception as e: self.log(f"Error in _process_analysis: {e}") self._send_error_message(params, locali.get_string("UNEXPECTED_ERROR").format(error=str(e))) def _validate_model_index(self, model_idx) -> int: try: idx = int(model_idx) return max(0, min(len(MODEL_API_NAMES) - 1, idx)) except (ValueError, TypeError): return 1 def _validate_temperature(self, temp_str) -> float: try: temp = float(temp_str) return max(0.0, min(2.0, temp)) except (ValueError, TypeError): return 0.7 def _validate_max_tokens(self, tokens_str) -> int: try: tokens = int(tokens_str) return max(100, min(32768, tokens)) except (ValueError, TypeError): return 4096 def _get_max_message_limit(self) -> int: try: limit = int(self.get_setting("max_message_limit", "50000")) return max(50, limit) except (ValueError, TypeError): return 50000 def _validate_message_count(self, count_str) -> int: try: count = int(count_str) max_limit = self._get_max_message_limit() return max(50, min(max_limit, count)) except (ValueError, TypeError): return 200 def _split_long_text(self, text: str, max_length: int = 3800) -> List[str]: if len(text) <= max_length: return [text] parts = [] current_pos = 0 while current_pos < len(text): end_pos = current_pos + max_length if end_pos >= len(text): parts.append(text[current_pos:]) break chunk = text[current_pos:end_pos] sentence_breaks = ['. ', '! ', '? ', '.\n', '!\n', '?\n'] best_break = -1 for break_char in sentence_breaks: last_break = chunk.rfind(break_char) if last_break > len(chunk) * 0.7: best_break = max(best_break, last_break + len(break_char)) if best_break == -1: paragraph_break = chunk.rfind('\n\n') if paragraph_break > len(chunk) * 0.5: best_break = paragraph_break + 2 if best_break == -1: line_break = chunk.rfind('\n') if line_break > len(chunk) * 0.5: best_break = line_break + 1 if best_break == -1: space_break = chunk.rfind(' ') if space_break > len(chunk) * 0.5: best_break = space_break + 1 if best_break == -1: best_break = max_length parts.append(text[current_pos:current_pos + best_break].rstrip()) current_pos += best_break return parts def _send_analysis_result(self, params: Any, analysis_text: str, message_count: int, is_summary: bool, was_truncated: bool = False): try: analysis_type = "Краткая сводка" if is_summary else "Подробный анализ" truncated_note = " (обрезано)" if was_truncated else "" header = f"🤖 **{analysis_type}** ({message_count} сообщений{truncated_note})\n\n" full_text = header + analysis_text auto_blockquote_enabled = self.get_setting("auto_blockquote", True) max_message_length = 3900 if len(full_text) <= max_message_length: use_blockquote = auto_blockquote_enabled and len(full_text) > 2000 self._send_single_message(params, full_text, use_blockquote) else: self._send_split_messages(params, header, analysis_text, auto_blockquote_enabled) success_msg = "✅ Анализ завершен" run_on_ui_thread(lambda: BulletinHelper.show_success(success_msg)) except Exception as e: self.log(f"Error sending analysis result: {e}") self._send_error_message(params, f"Ошибка отправки результата: {str(e)}") def _send_single_message(self, params: Any, text: str, use_blockquote: bool = False): try: try: parsed = parse_markdown(text) entities = [] if use_blockquote and parsed.text and len(parsed.text.strip()) > 0: blockquote_entity = TLRPC.TL_messageEntityBlockquote() blockquote_entity.collapsed = True blockquote_entity.offset = 0 try: blockquote_entity.length = len(parsed.text.encode('utf-16le')) // 2 except: blockquote_entity.length = len(parsed.text) entities.append(blockquote_entity) self.log(f"Added collapsible blockquote for message ({len(parsed.text)} chars)") if hasattr(parsed, 'entities') and parsed.entities: for entity in parsed.entities: try: tlrpc_entity = entity.to_tlrpc_object() if tlrpc_entity is not None: entities.append(tlrpc_entity) except Exception as entity_error: self.log(f"Error converting entity: {entity_error}") continue message_payload = { "peer": params.peer, "message": parsed.text, "entities": entities if entities else None } except Exception as parse_error: self.log(f"Error parsing markdown: {parse_error}") clean_text = text.replace("**", "").replace("*", "") message_payload = { "peer": params.peer, "message": clean_text } if use_blockquote: try: blockquote_entity = TLRPC.TL_messageEntityBlockquote() blockquote_entity.collapsed = True blockquote_entity.offset = 0 blockquote_entity.length = len(clean_text) message_payload["entities"] = [blockquote_entity] self.log("Added fallback blockquote for message") except Exception as blockquote_error: self.log(f"Error adding fallback blockquote: {blockquote_error}") if hasattr(params, 'replyToMsg') and params.replyToMsg: message_payload["replyToMsg"] = params.replyToMsg if hasattr(params, 'replyToTopMsg') and params.replyToTopMsg: message_payload["replyToTopMsg"] = params.replyToTopMsg send_message(message_payload) except Exception as e: self.log(f"Error sending single message: {e}") raise e def _send_split_messages(self, params: Any, header: str, analysis_text: str, auto_blockquote_enabled: bool): try: max_content_length = 3800 - len(header) - 50 text_parts = self._split_long_text(analysis_text, max_content_length) total_parts = len(text_parts) self.log(f"Splitting analysis into {total_parts} parts") for i, part in enumerate(text_parts, 1): if i == 1: part_header = header + f"**(Часть {i}/{total_parts})**\n\n" else: part_header = f"**(Часть {i}/{total_parts})**\n\n" full_part_text = part_header + part use_blockquote = auto_blockquote_enabled self._send_single_message(params, full_part_text, use_blockquote) if i < total_parts: import time time.sleep(0.5) except Exception as e: self.log(f"Error sending split messages: {e}") raise e def _send_error_message(self, params: Any, error_text: str): try: message_payload = { "peer": params.peer, "message": error_text } if hasattr(params, 'replyToMsg') and params.replyToMsg: message_payload["replyToMsg"] = params.replyToMsg if hasattr(params, 'replyToTopMsg') and params.replyToTopMsg: message_payload["replyToTopMsg"] = params.replyToTopMsg send_message(message_payload) except Exception as e: self.log(f"Error sending error message: {e}") run_on_ui_thread(lambda: BulletinHelper.show_error(error_text)) def _open_link(self, url: str): try: from android.content import Intent from android.net import Uri fragment = get_last_fragment() if not fragment: return context = fragment.getParentActivity() if not context: return intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) context.startActivity(intent) except Exception as e: self.log(f"Error opening link: {e}") def _handle_show_info_alert_click(self, view): try: title = locali.get_string("USAGE_INFO_TITLE") max_limit = self._get_max_message_limit() text = locali.get_string("USAGE_INFO_TEXT", max_limit=max_limit) fragment = get_last_fragment() if not fragment or not fragment.getParentActivity(): return context = fragment.getParentActivity() builder = AlertDialogBuilder(context, AlertDialogBuilder.ALERT_TYPE_MESSAGE) builder.set_title(title) builder.set_message(text) builder.set_positive_button("Закрыть", lambda d, w: builder.dismiss()) builder.set_cancelable(True) run_on_ui_thread(builder.show) except Exception as e: self.log(f"Error showing info alert: {e}") def create_settings(self) -> List[Any]: max_limit = self._get_max_message_limit() return [ Header(text=locali.get_string("SETTINGS_HEADER")), Switch( key="enabled", text=locali.get_string("ENABLE_SWITCH"), icon="ai_chat", default=True ), Input( key="gemini_api_key", text=locali.get_string("API_KEY_INPUT"), icon="msg_pin_code", default="", subtext=locali.get_string("API_KEY_SUBTEXT") ), Text( text=locali.get_string("GET_API_KEY_BUTTON"), icon="msg_link", accent=True, on_click=lambda view: self._open_link("https://aistudio.google.com/app/apikey") ), Divider(), Header(text="Настройки анализа"), Input( key="message_count", text=locali.get_string("MESSAGE_COUNT_INPUT"), icon="msg_voicechat_solar", default="200", subtext=locali.get_string("MESSAGE_COUNT_SUBTEXT", max_limit=max_limit) ), Input( key="max_message_limit", text=locali.get_string("MAX_MESSAGE_LIMIT_INPUT"), icon="msg_premium_limits", default="50000", subtext="Максимальное количество сообщений для анализа (без ограничений)" ), Selector( key="model_selection", text=locali.get_string("MODEL_SELECTOR"), icon="msg_language_solar", default=1, items=MODEL_DISPLAY_NAMES ), Divider(), Header(text="Промпты"), Input( key="analysis_prompt", text=locali.get_string("ANALYSIS_PROMPT_INPUT"), icon="msg_edit", default=DEFAULT_ANALYSIS_PROMPT ), Input( key="summary_prompt", text=locali.get_string("SUMMARY_PROMPT_INPUT"), icon="msg_message", default=DEFAULT_SUMMARY_PROMPT ), Input( key="report_prompt", text=locali.get_string("REPORT_PROMPT_INPUT"), icon="msg_report", default=IRONIC_REPORT_PROMPT ), Divider(), Header(text="Внешний вид"), Switch( key="auto_blockquote", text=locali.get_string("AUTO_BLOCKQUOTE_TITLE"), subtext=locali.get_string("AUTO_BLOCKQUOTE_SUBTEXT"), icon="header_goinline_solar", default=True ), Divider(), Header(text="Параметры генерации"), Input( key="temperature", text=locali.get_string("TEMPERATURE_INPUT"), icon="msg_settings", default="0.7", subtext=locali.get_string("TEMPERATURE_SUBTEXT") ), Input( key="max_tokens", text=locali.get_string("MAX_TOKENS_INPUT"), icon="msg_data", default="4096", subtext=locali.get_string("MAX_TOKENS_SUBTEXT") ), Divider(), Text( text=locali.get_string("USAGE_INFO_TITLE"), icon="msg_info", on_click=self._handle_show_info_alert_click ), ]