2125 lines
95 KiB
Plaintext
2125 lines
95 KiB
Plaintext
"""
|
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
|
⣿⣿⣿⣿⡿⠟⠋⠉⠉⠉⠉⠛⠿⣿⣿⣿⣿⡿⠛⠉⠉⠄⠈⠉⠙⠿⣿⣿⣿⣿
|
|
⣿⣿⡿⠋⠄⣠⣶⣿⣿⣿⣷⣦⣄⠈⠛⢟⢁⣠⣤⣴⣶⣤⣄⠄⠄⠄⠈⢿⣿⣿
|
|
⣿⡿⠁⢠⣾⣿⣿⣿⣿⣿⣿⣿⡿⣿⣦⣀⠈⠛⠛⠋⣸⣿⣿⣷⡄⠄⠄⠄⢻⣿
|
|
⣿⠁⢀⣿⣿⣿⣿⣿⣿⣿⠋⠄⠄⣿⣿⣿⣿⣶⣶⣾⣿⣿⣿⣿⣧⠄⠄⠄⠄⣿
|
|
⣿⠄⢸⣿⣿⣿⣿⣿⠟⠁⠄⠄⠄⠄⠙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⠄⠄⣿
|
|
⣿⠄⠘⣿⣿⣿⣿⡏⠄⠄⠄⠄⠄⠄⠄⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⠄⠄⣿
|
|
⣿⠄⠄⢻⣿⣿⣿⠁⠄⠄⠄⠄⠄⠄⠄⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⠄⢀⣿
|
|
⣿⡆⠄⠈⠿⠿⠋⠄⠄⠄⠄⠄⠄⢰⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠄⠄⠄⣸⣿
|
|
⣿⣿⡀⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⠄⣰⣿⣿
|
|
⣿⣿⣷⡄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠄⠄⣰⣿⣿⣿
|
|
⣿⣿⣿⣿⣄⠄⠄⠄⠄⠄⠄⠄⠄⣰⣿⣿⣿⣿⣿⣿⣿⣿⠏⠄⢀⣴⣿⣿⣿⣿
|
|
⣿⣿⣿⣿⣿⣷⣄⠄⠄⠄⠄⠄⣰⣿⣿⣿⣿⣿⣿⣿⡿⠃⠄⣠⣾⣿⣿⣿⣿⣿
|
|
⣿⣿⣿⣿⣿⣿⣿⣷⣄⠄⠄⠄⢿⣿⣿⣿⣿⣿⡿⠋⢀⣠⣾⣿⣿⣿⣿⣿⣿⣿
|
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣀⠄⠙⢿⣿⠟⠋⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣄⣨⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
|
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
|
|
|
by @mihailkotovski
|
|
Перед копированием/изменением кода уведомите @mihailkotovski
|
|
"""
|
|
|
|
import requests
|
|
import traceback
|
|
from typing import Any, Dict, Optional, List
|
|
from datetime import datetime
|
|
import weakref
|
|
|
|
from base_plugin import BasePlugin, XposedHook
|
|
from android_utils import OnClickListener, run_on_ui_thread
|
|
from client_utils import (
|
|
get_messages_controller, run_on_queue, send_request,
|
|
RequestCallback, get_last_fragment, get_user_config
|
|
)
|
|
from markdown_utils import parse_markdown
|
|
from ui.settings import Header, Input, Selector, Switch, Text, Divider
|
|
from ui.bulletin import BulletinHelper
|
|
from ui.alert import AlertDialogBuilder
|
|
from hook_utils import find_class, get_private_field
|
|
|
|
from java.util import Locale
|
|
from org.telegram.tgnet import TLRPC
|
|
from org.telegram.messenger import R, MessageObject
|
|
from org.telegram.ui import LaunchActivity
|
|
from org.telegram.ui.Components import EditTextBoldCursor
|
|
from org.telegram.ui.ActionBar import Theme
|
|
from android.text import InputType
|
|
|
|
__id__ = "unread_summary"
|
|
__name__ = "Unread Summary"
|
|
__description__ = "Summarize unread messages with AI providers by clicking the unread badge"
|
|
__author__ = "@mihailkotovski & @mishabotov"
|
|
__version__ = "1.0.0"
|
|
__icon__ = "pk_4377080_by_Ctikerubot/24"
|
|
__min_version__ = "12.1.1"
|
|
|
|
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-flash-latest",
|
|
"gemini-flash-lite-latest"
|
|
]
|
|
|
|
|
|
PROVIDER_TYPES = [
|
|
"Gemini",
|
|
"OpenAI-Compatible"
|
|
]
|
|
|
|
DEFAULT_UNREAD_SUMMARY_PROMPT_RU = """Проанализируй следующие непрочитанные сообщения и создай краткую сводку.
|
|
|
|
Сосредоточься на:
|
|
1. Основных темах обсуждения
|
|
2. Важной информации или решениях
|
|
3. Вопросах или задачах, адресованных пользователю
|
|
|
|
Сводка должна быть краткой (максимум 150 слов) и написана простым текстом без какого либо форматирования.
|
|
|
|
Сообщения для анализа:
|
|
{messages}
|
|
"""
|
|
|
|
DEFAULT_UNREAD_SUMMARY_PROMPT_EN = """Analyze the following unread messages and create a brief summary.
|
|
|
|
Focus on:
|
|
1. Main discussion topics
|
|
2. Important information or decisions
|
|
3. Questions or tasks addressed to the user
|
|
|
|
The summary should be concise (maximum 150 words) and written in plain text without any formatting.
|
|
|
|
Messages to analyze:
|
|
{messages}
|
|
"""
|
|
|
|
|
|
class LocalizationManager:
|
|
strings = {
|
|
"ru": {
|
|
"SETTINGS_HEADER": "Настройки суммаризации",
|
|
"API_KEY_INPUT": "API",
|
|
"API_KEY_SUBTEXT": "Получите ключ в Google AI Studio",
|
|
"GET_API_KEY_BUTTON": "Получить API ключ",
|
|
"MODEL_SELECTOR": "Модель Gemini",
|
|
"ENABLE_SWITCH": "Включить плагин",
|
|
"SUMMARY_PROMPT_INPUT": "Промпт для суммаризации",
|
|
"TEMPERATURE_INPUT": "Температура",
|
|
"TEMPERATURE_SUBTEXT": "0.0-2.0. Контролирует креативность",
|
|
"MAX_TOKENS_INPUT": "Максимум токенов",
|
|
"MAX_TOKENS_SUBTEXT": "Максимальная длина ответа",
|
|
"API_KEY_MISSING": "API ключ Gemini не найден. Укажите его в настройках.",
|
|
"ANALYZING_MESSAGE": "Анализирую непрочитанные сообщения...",
|
|
"API_ERROR": "Ошибка API: {error}",
|
|
"NO_UNREAD_MESSAGES": "Нет непрочитанных сообщений.",
|
|
"UNEXPECTED_ERROR": "Произошла ошибка: {error}",
|
|
"MENU_SUMMARIZE": "Суммаризовать непрочитанные",
|
|
"MENU_CANCEL": "Отмена",
|
|
"DIALOG_TITLE": "Сводка непрочитанных сообщений",
|
|
"CLOSE_BUTTON": "Закрыть",
|
|
"PLUGIN_DISABLED": "Плагин отключен в настройках",
|
|
"CANNOT_GET_DIALOG": "Не удалось определить текущий чат",
|
|
"FETCHING_MESSAGES": "Загружаю сообщения...",
|
|
"MENU_PROVIDER": "Провайдер",
|
|
"MENU_LANGUAGE": "Язык",
|
|
"MENU_AI_SETTINGS": "Настройки AI",
|
|
"MENU_SHARE": "Поделиться",
|
|
"SELECT_PROVIDER_TITLE": "Выбор провайдера",
|
|
"SELECT_LANGUAGE_TITLE": "Выбор языка",
|
|
"SELECTOR_ERROR": "Не удалось открыть селектор",
|
|
"AI_SETTINGS_HEADER": "Настройки AI",
|
|
"PROMPT_HEADER": "Промпт",
|
|
"PROVIDER_SELECTOR": "Провайдер",
|
|
"OPENAI_SECTION": "OpenAI-совместимый",
|
|
"OPENAI_API_KEY_INPUT": "API Key",
|
|
"OPENAI_BASE_URL_INPUT": "URL",
|
|
"OPENAI_MODEL_INPUT": "Модель",
|
|
"OPENAI_API_KEY_MISSING": "API ключ OpenAI не найден. Укажите его в настройках.",
|
|
"OPENAI_HEADER_NAME_INPUT": "Заголовок API-ключа",
|
|
"OPENAI_HEADER_PREFIX_INPUT": "Префикс заголовка",
|
|
"MESSAGE_LIMIT_INPUT": "Лимит сообщений",
|
|
"MESSAGE_LIMIT_SUBTEXT": "0 — без лимита. Сколько непрочитанных анализировать",
|
|
},
|
|
"en": {
|
|
"SETTINGS_HEADER": "Summary 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 Plugin",
|
|
"SUMMARY_PROMPT_INPUT": "Summary Prompt",
|
|
"TEMPERATURE_INPUT": "Temperature",
|
|
"TEMPERATURE_SUBTEXT": "0.0-2.0. Controls response creativity",
|
|
"MAX_TOKENS_INPUT": "Max Tokens",
|
|
"MAX_TOKENS_SUBTEXT": "Maximum response length",
|
|
"API_KEY_MISSING": "Gemini API key not found. Set it in settings.",
|
|
"ANALYZING_MESSAGE": "Analyzing unread messages...",
|
|
"API_ERROR": "AI API Error: {error}",
|
|
"NO_UNREAD_MESSAGES": "No unread messages found.",
|
|
"UNEXPECTED_ERROR": "An error occurred: {error}",
|
|
"MENU_SUMMARIZE": "Summarize Unread Messages",
|
|
"MENU_CANCEL": "Cancel",
|
|
"DIALOG_TITLE": "Unread Messages Summary",
|
|
"CLOSE_BUTTON": "Close",
|
|
"PLUGIN_DISABLED": "Plugin is disabled in settings",
|
|
"CANNOT_GET_DIALOG": "Cannot determine current chat",
|
|
"FETCHING_MESSAGES": "Fetching messages...",
|
|
"MENU_PROVIDER": "Provider",
|
|
"MENU_LANGUAGE": "Language",
|
|
"MENU_AI_SETTINGS": "AI Settings",
|
|
"MENU_SHARE": "Share",
|
|
"SELECT_PROVIDER_TITLE": "Select Provider",
|
|
"SELECT_LANGUAGE_TITLE": "Select Language",
|
|
"SELECTOR_ERROR": "Failed to open selector",
|
|
"AI_SETTINGS_HEADER": "AI Settings",
|
|
"PROMPT_HEADER": "Prompt",
|
|
"PROVIDER_SELECTOR": "Provider",
|
|
"OPENAI_SECTION": "OpenAI-compatible",
|
|
"OPENAI_API_KEY_INPUT": "API Key",
|
|
"OPENAI_BASE_URL_INPUT": "URL",
|
|
"OPENAI_MODEL_INPUT": "Model",
|
|
"OPENAI_API_KEY_MISSING": "OpenAI API key not found. Set it in settings.",
|
|
"OPENAI_HEADER_NAME_INPUT": "Header Name",
|
|
"OPENAI_HEADER_PREFIX_INPUT": "Header Prefix",
|
|
"MESSAGE_LIMIT_INPUT": "Message limit",
|
|
"MESSAGE_LIMIT_SUBTEXT": "0 = unlimited. How many unread messages to analyze",
|
|
}
|
|
}
|
|
|
|
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'))
|
|
|
|
try:
|
|
response = self.session.post(url, json=payload, timeout=90)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
if "candidates" not in 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:
|
|
return {"success": False, "error": "Empty candidates array in API response"}
|
|
|
|
first_candidate = candidates[0]
|
|
finish_reason = first_candidate.get("finishReason", "")
|
|
|
|
if 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:
|
|
return {"success": False, "error": "No content in API response"}
|
|
|
|
parts = content.get("parts", [])
|
|
if not parts or len(parts) == 0:
|
|
return {"success": False, "error": "No parts in content"}
|
|
|
|
text = parts[0].get("text", "")
|
|
if not text or not text.strip():
|
|
return {"success": False, "error": "Empty text in API response"}
|
|
|
|
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()
|
|
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:
|
|
return {"success": False, "error": f"Network error: {str(e)}"}
|
|
except Exception as e:
|
|
return {"success": False, "error": f"Unexpected error: {str(e)}"}
|
|
|
|
|
|
class OpenAICompatibleAPIHandler:
|
|
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, base_url: str, api_key: str, model_name: str, prompt: str,
|
|
temperature: float, max_tokens: int,
|
|
api_key_header: str = "Authorization", api_key_prefix: str = "Bearer") -> Dict[str, Any]:
|
|
try:
|
|
bu = (base_url or "https://api.openai.com").rstrip('/')
|
|
if bu.endswith("/v1") or bu.endswith("/v1/"):
|
|
url = bu.rstrip('/') + "/chat/completions"
|
|
else:
|
|
url = bu + "/v1/chat/completions"
|
|
|
|
headers = {}
|
|
if api_key_header:
|
|
if api_key_prefix:
|
|
headers[api_key_header] = f"{api_key_prefix} {api_key}"
|
|
else:
|
|
headers[api_key_header] = api_key
|
|
|
|
payload = {
|
|
"model": model_name,
|
|
"messages": [
|
|
{"role": "user", "content": prompt}
|
|
],
|
|
"temperature": float(temperature),
|
|
"max_tokens": int(max_tokens),
|
|
"stream": False
|
|
}
|
|
|
|
prompt_size = len(prompt.encode('utf-8'))
|
|
|
|
response = self.session.post(url, json=payload, headers=headers, timeout=90)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
if "choices" not in data or not data["choices"]:
|
|
err = data.get("error", {}).get("message", "No choices in API response")
|
|
return {"success": False, "error": err}
|
|
|
|
first_choice = data["choices"][0]
|
|
finish_reason = first_choice.get("finish_reason") or first_choice.get("finishReason", "")
|
|
if finish_reason and finish_reason not in ["stop", ""]:
|
|
if finish_reason == "length":
|
|
return {"success": False, "error": "Response truncated due to token limit"}
|
|
if finish_reason == "content_filter":
|
|
return {"success": False, "error": "Blocked by content filter"}
|
|
|
|
message = first_choice.get("message") or {}
|
|
text = (message.get("content") if isinstance(message, dict) else None) or first_choice.get("text", "")
|
|
if not text or not str(text).strip():
|
|
return {"success": False, "error": "Empty content in API response"}
|
|
|
|
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()
|
|
error_text += f": {error_json.get('error',{}).get('message', e.response.text)}"
|
|
except Exception:
|
|
error_text += f": {e.response.text}"
|
|
return {"success": False, "error": error_text}
|
|
except requests.exceptions.RequestException as e:
|
|
return {"success": False, "error": f"Network error: {str(e)}"}
|
|
except Exception as e:
|
|
return {"success": False, "error": f"Unexpected error: {str(e)}"}
|
|
|
|
|
|
class ChatUnreadCellConstructorHook(XposedHook):
|
|
def __init__(self, plugin: 'UnreadSummaryPlugin'):
|
|
super().__init__()
|
|
self.plugin = plugin
|
|
|
|
def after_hooked_method(self, param):
|
|
try:
|
|
unread_cell = param.thisObject
|
|
|
|
try:
|
|
from java.lang.ref import WeakReference as JWeakReference
|
|
_cell_wr = JWeakReference(unread_cell)
|
|
unread_cell_ref = (lambda ref=_cell_wr: ref.get())
|
|
except Exception:
|
|
try:
|
|
import weakref as _pyweakref
|
|
_cell_wr_py = _pyweakref.ref(unread_cell)
|
|
unread_cell_ref = (lambda ref=_cell_wr_py: ref())
|
|
except Exception:
|
|
unread_cell_ref = (lambda obj=unread_cell: obj)
|
|
|
|
def setup_click_listener():
|
|
try:
|
|
background_layout = None
|
|
|
|
cell_obj = unread_cell_ref()
|
|
if cell_obj is None:
|
|
return
|
|
|
|
background_layout = get_private_field(cell_obj, "backgroundLayout")
|
|
|
|
if not background_layout:
|
|
if cell_obj.getChildCount() > 0:
|
|
background_layout = cell_obj.getChildAt(0)
|
|
|
|
if not background_layout:
|
|
try:
|
|
field = cell_obj.getClass().getDeclaredField("backgroundLayout")
|
|
field.setAccessible(True)
|
|
background_layout = field.get(cell_obj)
|
|
except Exception:
|
|
pass
|
|
|
|
if background_layout:
|
|
from android.view import View
|
|
background_layout.setClickable(True)
|
|
background_layout.setFocusable(True)
|
|
|
|
def on_unread_cell_click(*_args, **_kwargs):
|
|
try:
|
|
if not self.plugin.is_analyzing:
|
|
try:
|
|
view_location = [0, 0]
|
|
background_layout.getLocationOnScreen(view_location)
|
|
center_x = view_location[0] + background_layout.getWidth() / 2.0
|
|
center_y = view_location[1] + background_layout.getHeight() / 2.0
|
|
LaunchActivity.makeRipple(center_x, center_y, 1.5)
|
|
|
|
try:
|
|
view_obj = _args[0] if (_args and _args[0] is not None) else background_layout
|
|
self.plugin._perform_click_vibration(view_obj)
|
|
except Exception:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
cell_for_action = unread_cell_ref()
|
|
if cell_for_action is None:
|
|
return
|
|
|
|
text_view = get_private_field(cell_for_action, "textView")
|
|
if text_view:
|
|
current_text = str(text_view.getText())
|
|
if ("•" in current_text and ("сообщений" in current_text or "message" in current_text)) and self.plugin.last_summary:
|
|
self.plugin._show_summary_bottom_sheet(self.plugin.last_summary, self.plugin.last_message_count)
|
|
return
|
|
|
|
self.plugin._start_summarization(cell_for_action)
|
|
except Exception as e:
|
|
pass
|
|
|
|
background_layout.setOnClickListener(OnClickListener(on_unread_cell_click))
|
|
else:
|
|
pass
|
|
|
|
except Exception:
|
|
pass
|
|
|
|
from android_utils import R
|
|
unread_cell.post(R(setup_click_listener))
|
|
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
class UnreadSummaryPlugin(BasePlugin):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.api_handler = GeminiAPIHandler()
|
|
self.openai_handler = OpenAICompatibleAPIHandler()
|
|
self.progress_dialog: Optional[AlertDialogBuilder] = None
|
|
self.hook_ref = None
|
|
self.last_summary = None
|
|
self.last_message_count = 0
|
|
self.locale_unhook = None
|
|
self.is_analyzing = False
|
|
|
|
def on_plugin_load(self):
|
|
self._apply_hook()
|
|
self._apply_locale_hook()
|
|
|
|
def on_plugin_unload(self):
|
|
self._remove_hook()
|
|
if self.progress_dialog:
|
|
run_on_ui_thread(lambda: self.progress_dialog.dismiss())
|
|
if self.locale_unhook:
|
|
try:
|
|
self.unhook_method(self.locale_unhook)
|
|
except Exception:
|
|
pass
|
|
finally:
|
|
self.locale_unhook = None
|
|
self.is_analyzing = False
|
|
|
|
def _apply_hook(self):
|
|
try:
|
|
self._remove_hook()
|
|
|
|
cls = find_class("org.telegram.ui.Cells.ChatUnreadCell")
|
|
if cls:
|
|
constructors = cls.getClass().getDeclaredConstructors()
|
|
if constructors and len(constructors) > 0:
|
|
self.hook_ref = self.hook_method(constructors[0], ChatUnreadCellConstructorHook(self))
|
|
else:
|
|
pass
|
|
else:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
def _apply_locale_hook(self):
|
|
try:
|
|
LocaleController = find_class("org.telegram.messenger.LocaleController")
|
|
if not LocaleController:
|
|
return
|
|
|
|
proceed_id = None
|
|
try:
|
|
proceed_id = R.string.Proceed
|
|
except Exception:
|
|
proceed_id = None
|
|
|
|
from java.lang import Integer
|
|
try:
|
|
getStringMethod = LocaleController.getClass().getDeclaredMethod("getString", Integer.TYPE)
|
|
except Exception as e:
|
|
return
|
|
|
|
class _LocaleHook(XposedHook):
|
|
def __init__(self, proceed_res_id):
|
|
super().__init__()
|
|
self.proceed_res_id = proceed_res_id
|
|
|
|
def after_hooked_method(self, param):
|
|
try:
|
|
if not param or not hasattr(param, 'args') or not param.args:
|
|
return
|
|
res_id = param.args[0]
|
|
if self.proceed_res_id is not None and res_id == self.proceed_res_id:
|
|
param.setResult("Summarize")
|
|
except Exception:
|
|
pass
|
|
|
|
self.locale_unhook = self.hook_method(getStringMethod, _LocaleHook(proceed_id))
|
|
if self.locale_unhook:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
def _remove_hook(self):
|
|
if self.hook_ref:
|
|
try:
|
|
self.unhook_method(self.hook_ref)
|
|
self.hook_ref = None
|
|
except Exception:
|
|
pass
|
|
|
|
def _show_summary_menu(self, unread_cell):
|
|
try:
|
|
self._start_summarization(unread_cell)
|
|
except Exception as e:
|
|
BulletinHelper.show_error(locali.get_string("UNEXPECTED_ERROR").format(error=str(e)))
|
|
|
|
def _start_summarization(self, unread_cell):
|
|
try:
|
|
if self.is_analyzing:
|
|
return
|
|
|
|
api_key = self.get_setting("gemini_api_key", "")
|
|
if not api_key:
|
|
BulletinHelper.show_error(locali.get_string("API_KEY_MISSING"))
|
|
return
|
|
|
|
dialog_id = self._get_current_dialog_id()
|
|
if not dialog_id:
|
|
BulletinHelper.show_error(locali.get_string("CANNOT_GET_DIALOG"))
|
|
return
|
|
|
|
self.is_analyzing = True
|
|
|
|
text_view = get_private_field(unread_cell, "textView")
|
|
if text_view:
|
|
run_on_ui_thread(lambda: text_view.setText("Анализирую..."))
|
|
|
|
run_on_queue(lambda: self._fetch_and_summarize(dialog_id, unread_cell))
|
|
|
|
except Exception as e:
|
|
self.is_analyzing = False
|
|
BulletinHelper.show_error(locali.get_string("UNEXPECTED_ERROR").format(error=str(e)))
|
|
|
|
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:
|
|
return None
|
|
|
|
def _get_current_topic_id(self) -> int:
|
|
try:
|
|
fragment = get_last_fragment()
|
|
if fragment and hasattr(fragment, 'getTopicId'):
|
|
topic_id = fragment.getTopicId()
|
|
return int(topic_id) if topic_id else 0
|
|
return 0
|
|
except Exception:
|
|
return 0
|
|
|
|
def _resolve_read_bounds(self, dialog_id: int, topic_id: int) -> Dict[str, int]:
|
|
in_max = 0
|
|
out_max = 0
|
|
unread_count = 0
|
|
|
|
try:
|
|
mc = get_messages_controller()
|
|
dialog = None
|
|
try:
|
|
dialog = mc.getDialog(dialog_id)
|
|
except Exception:
|
|
pass
|
|
|
|
if dialog:
|
|
try:
|
|
in_max = getattr(dialog, 'read_inbox_max_id', 0) or 0
|
|
out_max = getattr(dialog, 'read_outbox_max_id', 0) or 0
|
|
unread_count = getattr(dialog, 'unread_count', 0) or 0
|
|
except Exception:
|
|
pass
|
|
|
|
if topic_id and dialog_id < 0:
|
|
try:
|
|
chat_id = -int(dialog_id)
|
|
topics_controller = mc.getTopicsController()
|
|
if topics_controller:
|
|
topic = topics_controller.findTopic(chat_id, topic_id)
|
|
if topic:
|
|
in_max = getattr(topic, 'read_inbox_max_id', in_max) or in_max
|
|
out_max = getattr(topic, 'read_outbox_max_id', out_max) or out_max
|
|
unread_count = getattr(topic, 'unread_count', unread_count) or unread_count
|
|
except Exception:
|
|
pass
|
|
|
|
except Exception:
|
|
pass
|
|
return {"in_max": int(in_max or 0), "out_max": int(out_max or 0), "unread_count": int(unread_count or 0)}
|
|
|
|
def _fetch_and_summarize_for_dialog(self, dialog_id: int):
|
|
try:
|
|
if self.is_analyzing:
|
|
BulletinHelper.show_info("Анализ уже выполняется...")
|
|
return
|
|
|
|
self.is_analyzing = True
|
|
|
|
topic_id = self._get_current_topic_id()
|
|
read_bounds = self._resolve_read_bounds(dialog_id, topic_id)
|
|
|
|
def handle_messages(messages, error):
|
|
try:
|
|
if error:
|
|
BulletinHelper.show_error(f"Ошибка: {error}")
|
|
return
|
|
|
|
if not messages or len(messages) == 0:
|
|
BulletinHelper.show_error(locali.get_string("NO_UNREAD_MESSAGES"))
|
|
return
|
|
|
|
|
|
|
|
formatted_messages = self._format_messages_for_analysis(messages)
|
|
|
|
provider_type = self.get_setting("provider_type", 0)
|
|
try:
|
|
provider_type = int(provider_type)
|
|
except (ValueError, TypeError):
|
|
provider_type = 0
|
|
|
|
temperature = self._validate_temperature(self.get_setting("temperature", "0.7"))
|
|
max_tokens = self._validate_max_tokens(self.get_setting("max_tokens", "256000"))
|
|
|
|
default_prompt = self._get_default_prompt()
|
|
prompt_template = self.get_setting("summary_prompt", default_prompt)
|
|
final_prompt = prompt_template.format(messages=formatted_messages)
|
|
|
|
if provider_type == 1:
|
|
base_url = self.get_setting("openai_base_url", "https://api.openai.com").strip() or "https://api.openai.com"
|
|
api_key = self.get_setting("openai_api_key", "").strip()
|
|
if not api_key:
|
|
BulletinHelper.show_error(locali.get_string("OPENAI_API_KEY_MISSING"))
|
|
return
|
|
model_name = self.get_setting("openai_model", "gpt-5-chat").strip() or "gpt-5-chat"
|
|
header_name = self.get_setting("openai_api_key_header", "Authorization").strip() or "Authorization"
|
|
header_prefix = self.get_setting("openai_api_key_prefix", "Bearer").strip()
|
|
|
|
result = self.openai_handler.analyze_messages(base_url, api_key, model_name, final_prompt, temperature, max_tokens, header_name, header_prefix)
|
|
else:
|
|
api_key = self.get_setting("gemini_api_key", "").strip()
|
|
model_idx = self._validate_model_index(self.get_setting("model_selection", 1))
|
|
model_name = MODEL_API_NAMES[model_idx]
|
|
result = self.api_handler.analyze_messages(api_key, model_name, final_prompt, temperature, max_tokens)
|
|
|
|
if result.get("success"):
|
|
self.last_summary = result["text"]
|
|
self.last_message_count = len(messages)
|
|
run_on_ui_thread(lambda: self._show_summary_bottom_sheet(result["text"], len(messages)))
|
|
else:
|
|
error_msg = result.get("error", "Unknown")
|
|
BulletinHelper.show_error(locali.get_string("API_ERROR").format(error=error_msg))
|
|
|
|
except Exception as handle_error:
|
|
BulletinHelper.show_error(locali.get_string("UNEXPECTED_ERROR").format(error=str(handle_error)))
|
|
finally:
|
|
self.is_analyzing = False
|
|
|
|
|
|
self._fetch_unread_messages(dialog_id, handle_messages, read_bounds, topic_id)
|
|
|
|
except Exception as e:
|
|
self.is_analyzing = False
|
|
BulletinHelper.show_error(locali.get_string("UNEXPECTED_ERROR").format(error=str(e)))
|
|
|
|
def _fetch_and_summarize(self, dialog_id: int, unread_cell):
|
|
try:
|
|
topic_id = self._get_current_topic_id()
|
|
read_bounds = self._resolve_read_bounds(dialog_id, topic_id)
|
|
|
|
def handle_messages(messages, error):
|
|
try:
|
|
if error:
|
|
self._show_error_in_cell(unread_cell, error)
|
|
return
|
|
|
|
if not messages or len(messages) == 0:
|
|
self._show_error_in_cell(unread_cell, locali.get_string("NO_UNREAD_MESSAGES"))
|
|
return
|
|
|
|
|
|
|
|
formatted_messages = self._format_messages_for_analysis(messages)
|
|
|
|
provider_type = self.get_setting("provider_type", 0)
|
|
try:
|
|
provider_type = int(provider_type)
|
|
except (ValueError, TypeError):
|
|
provider_type = 0
|
|
|
|
temperature = self._validate_temperature(self.get_setting("temperature", "0.7"))
|
|
max_tokens = self._validate_max_tokens(self.get_setting("max_tokens", "256000"))
|
|
|
|
default_prompt = self._get_default_prompt()
|
|
prompt_template = self.get_setting("summary_prompt", default_prompt)
|
|
final_prompt = prompt_template.format(messages=formatted_messages)
|
|
|
|
if provider_type == 1:
|
|
base_url = self.get_setting("openai_base_url", "https://api.openai.com").strip() or "https://api.openai.com"
|
|
api_key = self.get_setting("openai_api_key", "").strip()
|
|
if not api_key:
|
|
self._show_error_in_cell(unread_cell, locali.get_string("OPENAI_API_KEY_MISSING"))
|
|
return
|
|
model_name = self.get_setting("openai_model", "gpt-5-chat").strip() or "gpt-5-chat"
|
|
header_name = self.get_setting("openai_api_key_header", "Authorization").strip() or "Authorization"
|
|
header_prefix = self.get_setting("openai_api_key_prefix", "Bearer").strip()
|
|
|
|
result = self.openai_handler.analyze_messages(base_url, api_key, model_name, final_prompt, temperature, max_tokens, header_name, header_prefix)
|
|
else:
|
|
api_key = self.get_setting("gemini_api_key", "").strip()
|
|
model_idx = self._validate_model_index(self.get_setting("model_selection", 1))
|
|
model_name = MODEL_API_NAMES[model_idx]
|
|
result = self.api_handler.analyze_messages(api_key, model_name, final_prompt, temperature, max_tokens)
|
|
|
|
if result.get("success"):
|
|
self._show_summary_in_cell(unread_cell, result["text"], len(messages))
|
|
else:
|
|
error_msg = result.get("error", "Unknown")
|
|
self._show_error_in_cell(unread_cell, locali.get_string("API_ERROR").format(error=error_msg))
|
|
|
|
except Exception as handle_error:
|
|
self._show_error_in_cell(unread_cell, locali.get_string("UNEXPECTED_ERROR").format(error=str(handle_error)))
|
|
finally:
|
|
self.is_analyzing = False
|
|
|
|
|
|
self._fetch_unread_messages(dialog_id, handle_messages, read_bounds, topic_id)
|
|
|
|
except Exception as e:
|
|
self.is_analyzing = False
|
|
self._show_error_in_cell(unread_cell, locali.get_string("UNEXPECTED_ERROR").format(error=str(e)))
|
|
|
|
def _fetch_unread_messages(self, dialog_id: int, callback, read_bounds: Dict[str, int], topic_id: int):
|
|
try:
|
|
message_limit = self._validate_message_limit(self.get_setting("message_limit", "0"))
|
|
self._fetch_messages_paginated(dialog_id, 0, [], {}, {}, callback, read_bounds, topic_id, message_limit)
|
|
|
|
except Exception as e:
|
|
callback(None, f"Error: {str(e)}")
|
|
|
|
def _fetch_messages_paginated(self, dialog_id: int, offset_id: int,
|
|
accumulated_messages: List, users: Dict, chats: Dict, callback,
|
|
read_bounds: Dict[str, int], topic_id: int, message_limit: int):
|
|
try:
|
|
current_limit = 100
|
|
|
|
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)
|
|
if accumulated_messages:
|
|
callback(accumulated_messages, None)
|
|
else:
|
|
callback(None, f"Error fetching messages: {error_msg}")
|
|
return
|
|
|
|
if not response or not hasattr(response, 'messages'):
|
|
if accumulated_messages:
|
|
callback(accumulated_messages, None)
|
|
else:
|
|
callback(None, "Empty response from server")
|
|
return
|
|
|
|
messages_count = response.messages.size()
|
|
|
|
if messages_count == 0:
|
|
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:
|
|
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:
|
|
continue
|
|
|
|
batch_messages = []
|
|
last_message_id = offset_id
|
|
found_read_message = False
|
|
|
|
in_max = int(read_bounds.get("in_max", 0) or 0)
|
|
out_max = int(read_bounds.get("out_max", 0) or 0)
|
|
unread_expected = int(read_bounds.get("unread_count", 0) or 0)
|
|
current_account = 0
|
|
try:
|
|
current_account = int(get_user_config().getCurrentAccount())
|
|
except Exception:
|
|
current_account = 0
|
|
|
|
for i in range(messages_count):
|
|
msg = response.messages.get(i)
|
|
try:
|
|
if hasattr(msg, 'action') and msg.action:
|
|
continue
|
|
|
|
if topic_id:
|
|
try:
|
|
msg_topic_id = MessageObject.getTopicId(current_account, msg, True)
|
|
if int(msg_topic_id) != int(topic_id):
|
|
continue
|
|
except Exception as e:
|
|
continue
|
|
|
|
try:
|
|
is_out = bool(getattr(msg, 'out', False))
|
|
msg_id = int(getattr(msg, 'id', 0) or 0)
|
|
unread_by_id = (msg_id > in_max) and (not is_out)
|
|
unread_by_flag = bool(getattr(msg, 'unread', False))
|
|
is_unread = unread_by_flag or unread_by_id
|
|
except Exception:
|
|
is_unread = False
|
|
|
|
if hasattr(msg, 'id') and msg.id <= in_max:
|
|
found_read_message = True
|
|
break
|
|
|
|
message_text = self._format_message_text_or_media(msg)
|
|
if not message_text:
|
|
continue
|
|
|
|
if not is_unread:
|
|
continue
|
|
|
|
sender_name = self._get_sender_name(msg, users, chats)
|
|
msg_time = self._format_message_time(msg)
|
|
|
|
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:
|
|
continue
|
|
|
|
accumulated_messages.extend(batch_messages)
|
|
|
|
reached_limit = (message_limit > 0 and len(accumulated_messages) >= message_limit)
|
|
should_finish = (unread_expected > 0 and len(accumulated_messages) >= unread_expected) or found_read_message or len(batch_messages) == 0 or messages_count < current_limit or reached_limit
|
|
if should_finish:
|
|
result_messages = accumulated_messages
|
|
if unread_expected > 0 and len(result_messages) > unread_expected:
|
|
try:
|
|
try:
|
|
result_messages = sorted(result_messages, key=lambda m: m.get('id', 0))
|
|
except Exception:
|
|
pass
|
|
result_messages = result_messages[:unread_expected]
|
|
except Exception:
|
|
pass
|
|
if message_limit > 0 and len(result_messages) > message_limit:
|
|
try:
|
|
try:
|
|
result_messages = sorted(result_messages, key=lambda m: m.get('id', 0))
|
|
except Exception:
|
|
pass
|
|
result_messages = result_messages[:message_limit]
|
|
except Exception:
|
|
pass
|
|
callback(result_messages, None)
|
|
else:
|
|
self._fetch_messages_paginated(dialog_id, last_message_id,
|
|
accumulated_messages, users, chats, callback,
|
|
read_bounds, topic_id, message_limit)
|
|
|
|
except Exception as response_error:
|
|
if accumulated_messages:
|
|
callback(accumulated_messages, None)
|
|
else:
|
|
callback(None, f"Error processing response: {str(response_error)}")
|
|
|
|
request_callback = RequestCallback(handle_response)
|
|
send_request(req, request_callback)
|
|
|
|
except Exception as e:
|
|
if accumulated_messages:
|
|
callback(accumulated_messages, None)
|
|
else:
|
|
callback(None, f"Error: {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:
|
|
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:
|
|
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:
|
|
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 _get_media_label(self, msg) -> str:
|
|
try:
|
|
if not hasattr(msg, 'media') or msg.media is None:
|
|
return ""
|
|
media = msg.media
|
|
cls = None
|
|
try:
|
|
cls = media.getClass().getSimpleName()
|
|
except Exception:
|
|
cls = str(media.__class__.__name__)
|
|
|
|
if hasattr(media, 'photo') and media.photo is not None:
|
|
return "[Photo]"
|
|
if hasattr(media, 'document') and media.document is not None:
|
|
try:
|
|
mime = getattr(media.document, 'mime_type', None) or ""
|
|
if 'gif' in mime.lower():
|
|
return "[GIF]"
|
|
if mime.startswith('video/'):
|
|
return "[Video]"
|
|
if mime.startswith('audio/'):
|
|
return "[Audio]"
|
|
return "[File]"
|
|
except Exception:
|
|
return "[Document]"
|
|
if 'WebPage' in cls:
|
|
return "[Link]"
|
|
if 'Geo' in cls or 'Venue' in cls:
|
|
return "[Location]"
|
|
if 'Contact' in cls:
|
|
return "[Contact]"
|
|
if 'Poll' in cls:
|
|
return "[Poll]"
|
|
return "[Media]"
|
|
except Exception:
|
|
return "[Media]"
|
|
|
|
def _format_message_text_or_media(self, msg) -> str:
|
|
try:
|
|
text = getattr(msg, 'message', None)
|
|
if text and str(text).strip():
|
|
return str(text)
|
|
label = self._get_media_label(msg)
|
|
return label
|
|
except Exception:
|
|
return ""
|
|
|
|
def _get_default_prompt(self) -> str:
|
|
lang_index = self.get_setting("summary_language", 0)
|
|
try:
|
|
lang_index = int(lang_index)
|
|
except (ValueError, TypeError):
|
|
lang_index = 0
|
|
return DEFAULT_UNREAD_SUMMARY_PROMPT_RU if lang_index == 0 else DEFAULT_UNREAD_SUMMARY_PROMPT_EN
|
|
|
|
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 2048
|
|
|
|
def _validate_message_limit(self, limit_str) -> int:
|
|
try:
|
|
value = int(limit_str)
|
|
if value <= 0:
|
|
return 0
|
|
return max(1, min(100000, value))
|
|
except (ValueError, TypeError):
|
|
return 0
|
|
|
|
def _show_summary_in_cell(self, unread_cell, summary_text: str, message_count: int):
|
|
self.last_summary = summary_text
|
|
self.last_message_count = message_count
|
|
|
|
def update_and_show():
|
|
try:
|
|
text_view = get_private_field(unread_cell, "textView")
|
|
if text_view:
|
|
preview = self._make_summary_preview(summary_text, message_count)
|
|
text_view.setText(preview)
|
|
BulletinHelper.show_success(f"Проанализировано {message_count} сообщений")
|
|
|
|
except Exception as e:
|
|
BulletinHelper.show_error(f"Ошибка: {str(e)}")
|
|
|
|
run_on_ui_thread(update_and_show)
|
|
|
|
def _make_summary_preview(self, summary_text: str, message_count: int) -> str:
|
|
try:
|
|
if locali.language == "ru":
|
|
msg_word = "сообщений"
|
|
else:
|
|
msg_word = "messages" if message_count != 1 else "message"
|
|
return f"{message_count} • {msg_word}"
|
|
except Exception:
|
|
return str(message_count)
|
|
|
|
def _show_dots_menu(self, anchor_view, summary_text: str):
|
|
try:
|
|
from org.telegram.ui.ActionBar import ActionBarPopupWindow, Theme
|
|
from org.telegram.ui.Components import LayoutHelper
|
|
from org.telegram.messenger import AndroidUtilities, R
|
|
from android.view import View, Gravity
|
|
from android.widget import FrameLayout, LinearLayout, TextView, ImageView
|
|
from android.util import TypedValue
|
|
from androidx.core.content import ContextCompat
|
|
|
|
context = anchor_view.getContext()
|
|
|
|
popup_layout = ActionBarPopupWindow.ActionBarPopupWindowLayout(context)
|
|
popup_layout.setBackgroundColor(Theme.getColor(Theme.key_actionBarDefaultSubmenuBackground))
|
|
popup_layout.setFitItems(True)
|
|
|
|
def create_menu_item(icon_res: int, title: str, on_click_action):
|
|
item_frame = FrameLayout(context)
|
|
item_frame.setMinimumWidth(AndroidUtilities.dp(200))
|
|
item_frame.setClickable(True)
|
|
item_frame.setFocusable(True)
|
|
item_frame.setBackground(Theme.createSelectorDrawable(Theme.getColor(Theme.key_listSelector), 2))
|
|
|
|
item_content = LinearLayout(context)
|
|
item_content.setOrientation(LinearLayout.HORIZONTAL)
|
|
item_content.setGravity(Gravity.CENTER_VERTICAL)
|
|
item_content.setPadding(AndroidUtilities.dp(16), AndroidUtilities.dp(12), AndroidUtilities.dp(16), AndroidUtilities.dp(12))
|
|
|
|
icon = ImageView(context)
|
|
icon.setScaleType(ImageView.ScaleType.CENTER)
|
|
try:
|
|
icon_drawable = ContextCompat.getDrawable(context, icon_res)
|
|
from android.graphics import PorterDuff
|
|
icon_drawable.setColorFilter(Theme.getColor(Theme.key_actionBarDefaultSubmenuItem), PorterDuff.Mode.SRC_IN)
|
|
icon.setImageDrawable(icon_drawable)
|
|
except Exception:
|
|
icon.setImageResource(icon_res)
|
|
item_content.addView(icon, LayoutHelper.createLinear(24, 24, Gravity.CENTER_VERTICAL, 0, 0, 12, 0))
|
|
|
|
title_tv = TextView(context)
|
|
title_tv.setText(title)
|
|
title_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16)
|
|
try:
|
|
title_tv.setTextColor(Theme.getColor(Theme.key_actionBarDefaultSubmenuItem))
|
|
except Exception:
|
|
pass
|
|
item_content.addView(title_tv, LayoutHelper.createLinear(-1, -2, 1.0, Gravity.CENTER_VERTICAL))
|
|
|
|
item_frame.addView(item_content)
|
|
return item_frame, on_click_action
|
|
|
|
menu_items = [
|
|
(R.drawable.msg_bot, locali.get_string("MENU_PROVIDER"), lambda: self._show_provider_selector()),
|
|
(R.drawable.msg_translate, locali.get_string("MENU_LANGUAGE"), lambda: self._show_language_selector()),
|
|
(R.drawable.msg_share, locali.get_string("MENU_SHARE"), lambda: self._share_summary(summary_text)),
|
|
]
|
|
|
|
popup_window_ref = [None]
|
|
|
|
for icon_res, title, action in menu_items:
|
|
item_view, on_click_action = create_menu_item(icon_res, title, action)
|
|
|
|
item_view.setOnClickListener(OnClickListener(lambda *_args, act=on_click_action, pw_ref=popup_window_ref: (
|
|
pw_ref[0].dismiss() if pw_ref[0] else None,
|
|
act()
|
|
)))
|
|
|
|
popup_layout.addView(item_view, LayoutHelper.createLinear(-1, -2))
|
|
|
|
popup_window = ActionBarPopupWindow(
|
|
popup_layout,
|
|
-2,
|
|
-2
|
|
)
|
|
popup_window_ref[0] = popup_window
|
|
|
|
popup_window.setOutsideTouchable(True)
|
|
popup_window.setClippingEnabled(True)
|
|
popup_window.setAnimationStyle(R.style.PopupContextAnimation)
|
|
popup_window.setFocusable(True)
|
|
|
|
popup_layout.measure(
|
|
View.MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(1000), View.MeasureSpec.AT_MOST),
|
|
View.MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(1000), View.MeasureSpec.AT_MOST)
|
|
)
|
|
|
|
location = [0, 0]
|
|
anchor_view.getLocationInWindow(location)
|
|
|
|
popup_x = location[0] + anchor_view.getWidth() - popup_layout.getMeasuredWidth()
|
|
popup_y = location[1] - popup_layout.getMeasuredHeight()
|
|
|
|
popup_window.showAtLocation(
|
|
anchor_view,
|
|
Gravity.TOP | Gravity.LEFT,
|
|
popup_x,
|
|
popup_y
|
|
)
|
|
|
|
popup_window.dimBehind()
|
|
|
|
except Exception as e:
|
|
BulletinHelper.show_error(f"Ошибка: {str(e)}")
|
|
|
|
def _select_provider(self, index: int):
|
|
try:
|
|
self.set_setting("provider_type", index)
|
|
provider_name = PROVIDER_TYPES[index] if 0 <= index < len(PROVIDER_TYPES) else str(index)
|
|
msg = f"Выбран провайдер: {provider_name}" if locali.language == "ru" else f"Provider selected: {provider_name}"
|
|
BulletinHelper.show_success(msg)
|
|
except Exception as e:
|
|
pass
|
|
|
|
def _select_language(self, index: int):
|
|
try:
|
|
current_prompt = self.get_setting("summary_prompt", "")
|
|
old_lang_index = self.get_setting("summary_language", 0)
|
|
try:
|
|
old_lang_index = int(old_lang_index)
|
|
except (ValueError, TypeError):
|
|
old_lang_index = 0
|
|
|
|
old_default = DEFAULT_UNREAD_SUMMARY_PROMPT_RU if old_lang_index == 0 else DEFAULT_UNREAD_SUMMARY_PROMPT_EN
|
|
new_default = DEFAULT_UNREAD_SUMMARY_PROMPT_RU if index == 0 else DEFAULT_UNREAD_SUMMARY_PROMPT_EN
|
|
|
|
self.set_setting("summary_language", index)
|
|
|
|
if not current_prompt or current_prompt.strip() == old_default.strip():
|
|
self.set_setting("summary_prompt", new_default)
|
|
|
|
lang_text = "Русский" if index == 0 else "English"
|
|
BulletinHelper.show_success(f"Язык изменен: {lang_text}")
|
|
except Exception as e:
|
|
pass
|
|
|
|
def _share_summary(self, summary_text: str):
|
|
try:
|
|
fragment = get_last_fragment()
|
|
if not fragment:
|
|
BulletinHelper.show_error("Не удалось открыть диалог выбора")
|
|
return
|
|
|
|
from org.telegram.ui import LaunchActivity
|
|
from android.content import Intent
|
|
|
|
try:
|
|
activity = fragment.getParentActivity()
|
|
if activity and isinstance(activity, LaunchActivity):
|
|
share_text = f"📝 Сводка непрочитанных сообщений:\n\n{summary_text}"
|
|
|
|
intent = Intent(Intent.ACTION_SEND)
|
|
intent.setType("text/plain")
|
|
intent.putExtra(Intent.EXTRA_TEXT, share_text)
|
|
|
|
chooser = Intent.createChooser(intent, "Поделиться сводкой")
|
|
activity.startActivity(chooser)
|
|
else:
|
|
from android.content import ClipData, ClipboardManager, Context
|
|
clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE)
|
|
clip = ClipData.newPlainText("Summary", summary_text)
|
|
clipboard.setPrimaryClip(clip)
|
|
BulletinHelper.show_success("Сводка скопирована в буфер обмена")
|
|
except Exception as share_error:
|
|
from android.content import ClipData, ClipboardManager, Context
|
|
context = fragment.getParentActivity()
|
|
clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE)
|
|
clip = ClipData.newPlainText("Summary", summary_text)
|
|
clipboard.setPrimaryClip(clip)
|
|
BulletinHelper.show_success("📋 Сводка скопирована в буфер обмена")
|
|
|
|
except Exception as e:
|
|
BulletinHelper.show_error("Ошибка при попытке поделиться")
|
|
|
|
def _show_provider_selector(self):
|
|
try:
|
|
fragment = get_last_fragment()
|
|
if not fragment or not hasattr(fragment, 'getParentActivity'):
|
|
BulletinHelper.show_error(locali.get_string("SELECTOR_ERROR"))
|
|
return
|
|
|
|
context = fragment.getParentActivity()
|
|
if not context:
|
|
BulletinHelper.show_error(locali.get_string("SELECTOR_ERROR"))
|
|
return
|
|
|
|
builder = AlertDialogBuilder(context, AlertDialogBuilder.ALERT_TYPE_MESSAGE)
|
|
builder.set_title(locali.get_string("SELECT_PROVIDER_TITLE"))
|
|
|
|
def on_provider_selected(dialog_builder, which):
|
|
self._select_provider(which)
|
|
dialog_builder.dismiss()
|
|
|
|
builder.set_items(PROVIDER_TYPES, on_provider_selected)
|
|
builder.show()
|
|
|
|
except Exception as e:
|
|
BulletinHelper.show_error(locali.get_string("SELECTOR_ERROR"))
|
|
|
|
def _show_language_selector(self):
|
|
try:
|
|
fragment = get_last_fragment()
|
|
if not fragment or not hasattr(fragment, 'getParentActivity'):
|
|
BulletinHelper.show_error(locali.get_string("SELECTOR_ERROR"))
|
|
return
|
|
|
|
context = fragment.getParentActivity()
|
|
if not context:
|
|
BulletinHelper.show_error(locali.get_string("SELECTOR_ERROR"))
|
|
return
|
|
|
|
current_lang_index = self.get_setting("summary_language", 0)
|
|
try:
|
|
current_lang_index = int(current_lang_index)
|
|
except (ValueError, TypeError):
|
|
current_lang_index = 0
|
|
|
|
lang_items = ["🇷🇺 Русский", "🇬🇧 English"]
|
|
|
|
builder = AlertDialogBuilder(context, AlertDialogBuilder.ALERT_TYPE_MESSAGE)
|
|
builder.set_title(locali.get_string("SELECT_LANGUAGE_TITLE"))
|
|
|
|
def on_language_selected(dialog_builder, which):
|
|
self._select_language(which)
|
|
dialog_builder.dismiss()
|
|
|
|
builder.set_items(lang_items, on_language_selected)
|
|
builder.show()
|
|
|
|
except Exception as e:
|
|
BulletinHelper.show_error(locali.get_string("SELECTOR_ERROR"))
|
|
|
|
def _show_summary_bottom_sheet(self, summary_text: str, message_count: int):
|
|
try:
|
|
fragment = get_last_fragment()
|
|
if not fragment or not hasattr(fragment, 'getParentActivity'):
|
|
BulletinHelper.show_error("Не удалось показать сводку")
|
|
return
|
|
context = fragment.getParentActivity()
|
|
if not context:
|
|
BulletinHelper.show_error("Не удалось показать сводку")
|
|
return
|
|
|
|
from org.telegram.ui.ActionBar import BottomSheet, Theme
|
|
from android.widget import LinearLayout, TextView, ScrollView, FrameLayout
|
|
from android.view import Gravity, View
|
|
from android.util import TypedValue
|
|
from org.telegram.ui.Components import LayoutHelper
|
|
from org.telegram.messenger import AndroidUtilities
|
|
from android.graphics.drawable import GradientDrawable
|
|
from android.graphics import Color
|
|
|
|
sheet = BottomSheet(context, False)
|
|
root_layout = LinearLayout(context)
|
|
root_layout.setOrientation(LinearLayout.VERTICAL)
|
|
root_layout.setPadding(AndroidUtilities.dp(20), AndroidUtilities.dp(16), AndroidUtilities.dp(20), AndroidUtilities.dp(20))
|
|
try:
|
|
root_layout.setBackgroundColor(Theme.getColor(Theme.key_dialogBackground))
|
|
except Exception:
|
|
pass
|
|
|
|
title_view = TextView(context)
|
|
title_view.setTypeface(AndroidUtilities.bold())
|
|
title_view.setGravity(Gravity.LEFT)
|
|
title_view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20)
|
|
title_view.setText(locali.get_string('DIALOG_TITLE'))
|
|
try:
|
|
title_view.setTextColor(Theme.getColor(Theme.key_dialogTextBlack))
|
|
except Exception:
|
|
pass
|
|
root_layout.addView(title_view, LayoutHelper.createLinear(-1, -2, Gravity.LEFT, 0, 0, 0, 12))
|
|
|
|
tags_container = LinearLayout(context)
|
|
tags_container.setOrientation(LinearLayout.HORIZONTAL)
|
|
tags_container.setGravity(Gravity.LEFT)
|
|
|
|
def create_tag(text: str, bg_color: int):
|
|
tag_frame = FrameLayout(context)
|
|
tag_bg = GradientDrawable()
|
|
tag_bg.setCornerRadius(AndroidUtilities.dp(12))
|
|
tag_bg.setColor(bg_color)
|
|
tag_frame.setBackground(tag_bg)
|
|
|
|
tag_text = TextView(context)
|
|
tag_text.setText(text)
|
|
tag_text.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13)
|
|
tag_text.setTypeface(AndroidUtilities.bold())
|
|
try:
|
|
tag_text.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlueText))
|
|
except Exception:
|
|
tag_text.setTextColor(Color.parseColor("#3390EC"))
|
|
tag_text.setPadding(AndroidUtilities.dp(10), AndroidUtilities.dp(4), AndroidUtilities.dp(10), AndroidUtilities.dp(4))
|
|
|
|
tag_frame.addView(tag_text)
|
|
return tag_frame
|
|
|
|
lang_index = self.get_setting("summary_language", 0)
|
|
try:
|
|
lang_index = int(lang_index)
|
|
except (ValueError, TypeError):
|
|
lang_index = 0
|
|
lang_display = "RU" if lang_index == 0 else "EN"
|
|
try:
|
|
tag_bg_color = Theme.getColor(Theme.key_chat_inLoader) & 0x30FFFFFF | 0x20000000
|
|
except Exception:
|
|
tag_bg_color = Color.parseColor("#E8F4FC")
|
|
|
|
lang_tag = create_tag(lang_display, tag_bg_color)
|
|
tags_container.addView(lang_tag, LayoutHelper.createLinear(-2, -2, 0, 0, 8, 0))
|
|
|
|
provider_type = self.get_setting("provider_type", 0)
|
|
try:
|
|
provider_type = int(provider_type)
|
|
except (ValueError, TypeError):
|
|
provider_type = 0
|
|
if provider_type == 1:
|
|
provider_display = "OpenAI"
|
|
else:
|
|
model_idx = self._validate_model_index(self.get_setting("model_selection", 1))
|
|
provider_display = MODEL_DISPLAY_NAMES[model_idx].split()[0]
|
|
model_tag = create_tag(provider_display, tag_bg_color)
|
|
tags_container.addView(model_tag, LayoutHelper.createLinear(-2, -2))
|
|
|
|
root_layout.addView(tags_container, LayoutHelper.createLinear(-1, -2, 0, 0, 0, 8))
|
|
|
|
message_info = TextView(context)
|
|
message_info.setText(f"Проанализировано сообщений: {message_count}")
|
|
message_info.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13)
|
|
try:
|
|
message_info.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText))
|
|
except Exception:
|
|
message_info.setTextColor(Color.parseColor("#808080"))
|
|
root_layout.addView(message_info, LayoutHelper.createLinear(-1, -2, 0, 0, 0, 12))
|
|
|
|
body_scroll = ScrollView(context)
|
|
body_scroll.setVerticalScrollBarEnabled(False)
|
|
body_scroll.setPadding(AndroidUtilities.dp(4), 0, AndroidUtilities.dp(4), 0)
|
|
|
|
body_tv = TextView(context)
|
|
try:
|
|
parsed = parse_markdown(summary_text)
|
|
body_tv.setText(parsed.text)
|
|
except Exception:
|
|
body_tv.setText(summary_text)
|
|
body_tv.setTextIsSelectable(True)
|
|
body_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15)
|
|
try:
|
|
body_tv.setTextColor(Theme.getColor(Theme.key_dialogTextBlack))
|
|
except Exception:
|
|
pass
|
|
try:
|
|
body_tv.setLineSpacing(AndroidUtilities.dp(4), 1.15)
|
|
except Exception:
|
|
pass
|
|
|
|
body_scroll.addView(body_tv)
|
|
|
|
root_layout.addView(body_scroll, LayoutHelper.createLinear(-1, 0, 1.0))
|
|
|
|
divider = View(context)
|
|
try:
|
|
divider_color = Theme.getColor(Theme.key_divider)
|
|
except Exception:
|
|
divider_color = Color.parseColor("#E0E0E0")
|
|
divider.setBackgroundColor(divider_color)
|
|
root_layout.addView(divider, LayoutHelper.createLinear(-1, 1, 0, 16, 0, 12))
|
|
|
|
def create_action_button(icon_res: str, on_click):
|
|
btn_frame = FrameLayout(context)
|
|
btn_bg = GradientDrawable()
|
|
btn_bg.setCornerRadius(AndroidUtilities.dp(20))
|
|
try:
|
|
btn_bg.setColor(Theme.getColor(Theme.key_chat_inLoader) & 0x15FFFFFF | 0x0A000000)
|
|
except Exception:
|
|
btn_bg.setColor(Color.parseColor("#F5F5F5"))
|
|
btn_frame.setBackground(btn_bg)
|
|
btn_frame.setPadding(AndroidUtilities.dp(16), AndroidUtilities.dp(10), AndroidUtilities.dp(16), AndroidUtilities.dp(10))
|
|
btn_frame.setClickable(True)
|
|
btn_frame.setFocusable(True)
|
|
|
|
btn_content = LinearLayout(context)
|
|
btn_content.setOrientation(LinearLayout.HORIZONTAL)
|
|
btn_content.setGravity(Gravity.CENTER)
|
|
|
|
try:
|
|
icon_id = R.drawable.__getattribute__(R.drawable, icon_res)
|
|
icon_view = TextView(context)
|
|
icon_view.setCompoundDrawablesWithIntrinsicBounds(icon_id, 0, 0, 0)
|
|
icon_view.setCompoundDrawablePadding(AndroidUtilities.dp(4))
|
|
except Exception:
|
|
icon_view = TextView(context)
|
|
|
|
btn_content.addView(icon_view)
|
|
btn_frame.addView(btn_content)
|
|
btn_frame.setOnClickListener(OnClickListener(lambda *_: on_click(btn_frame)))
|
|
return btn_frame
|
|
|
|
actions_row = LinearLayout(context)
|
|
actions_row.setOrientation(LinearLayout.HORIZONTAL)
|
|
actions_row.setGravity(Gravity.CENTER_VERTICAL)
|
|
|
|
def on_repeat(v):
|
|
try:
|
|
sheet.dismiss()
|
|
dialog_id = self._get_current_dialog_id()
|
|
if dialog_id:
|
|
BulletinHelper.show_info("Обновляю сводку...")
|
|
self.last_summary = None
|
|
self.last_message_count = 0
|
|
run_on_queue(lambda: self._fetch_and_summarize_for_dialog(dialog_id))
|
|
else:
|
|
BulletinHelper.show_error("Не удалось определить текущий чат")
|
|
except Exception as e:
|
|
pass
|
|
|
|
def on_copy(v):
|
|
try:
|
|
from android.content import ClipData, ClipboardManager, Context
|
|
clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE)
|
|
clip = ClipData.newPlainText("Unread summary", summary_text)
|
|
clipboard.setPrimaryClip(clip)
|
|
BulletinHelper.show_success("Скопировано в буфер обмена")
|
|
except Exception as copy_error:
|
|
BulletinHelper.show_error("Ошибка копирования")
|
|
|
|
def on_more(v):
|
|
try:
|
|
self._show_dots_menu(v, summary_text)
|
|
except Exception as e:
|
|
BulletinHelper.show_error("Ошибка открытия меню")
|
|
|
|
def create_rounded_button(icon_res: int, text: str, on_click):
|
|
from androidx.core.content import ContextCompat
|
|
|
|
btn_frame = FrameLayout(context)
|
|
btn_bg = GradientDrawable()
|
|
btn_bg.setCornerRadius(AndroidUtilities.dp(18))
|
|
try:
|
|
bg_color = Theme.getColor(Theme.key_chat_inLoader) & 0x20FFFFFF | 0x10000000
|
|
except Exception:
|
|
bg_color = Color.parseColor("#F0F0F0")
|
|
btn_bg.setColor(bg_color)
|
|
|
|
try:
|
|
from android.graphics.drawable import RippleDrawable
|
|
from android.content.res import ColorStateList
|
|
ripple_color = ColorStateList.valueOf(Color.parseColor("#40000000"))
|
|
ripple_drawable = RippleDrawable(ripple_color, btn_bg, None)
|
|
btn_frame.setBackground(ripple_drawable)
|
|
except Exception:
|
|
btn_frame.setBackground(btn_bg)
|
|
|
|
btn_layout = LinearLayout(context)
|
|
btn_layout.setOrientation(LinearLayout.HORIZONTAL)
|
|
btn_layout.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL)
|
|
btn_layout.setPadding(AndroidUtilities.dp(14), AndroidUtilities.dp(10), AndroidUtilities.dp(14), AndroidUtilities.dp(10))
|
|
btn_layout.setMinimumHeight(AndroidUtilities.dp(40))
|
|
|
|
if icon_res:
|
|
from android.widget import ImageView
|
|
icon_view = ImageView(context)
|
|
icon_view.setScaleType(ImageView.ScaleType.FIT_CENTER)
|
|
try:
|
|
from android.graphics import PorterDuff
|
|
icon_drawable = ContextCompat.getDrawable(context, icon_res)
|
|
icon_drawable.setColorFilter(Theme.getColor(Theme.key_dialogTextBlack), PorterDuff.Mode.SRC_IN)
|
|
icon_view.setImageDrawable(icon_drawable)
|
|
except Exception:
|
|
icon_view.setImageResource(icon_res)
|
|
btn_layout.addView(icon_view, LayoutHelper.createLinear(16, 16, Gravity.CENTER_VERTICAL))
|
|
|
|
if text:
|
|
spacer = View(context)
|
|
btn_layout.addView(spacer, LayoutHelper.createLinear(6, 0))
|
|
|
|
if text:
|
|
label_text = TextView(context)
|
|
label_text.setText(text)
|
|
label_text.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14)
|
|
label_text.setGravity(Gravity.CENTER_VERTICAL)
|
|
try:
|
|
label_text.setTextColor(Theme.getColor(Theme.key_dialogTextBlack))
|
|
except Exception:
|
|
label_text.setTextColor(Color.parseColor("#000000"))
|
|
btn_layout.addView(label_text, LayoutHelper.createLinear(-2, -2, Gravity.CENTER_VERTICAL))
|
|
|
|
btn_frame.addView(btn_layout)
|
|
btn_frame.setClickable(True)
|
|
btn_frame.setFocusable(True)
|
|
btn_frame.setOnClickListener(OnClickListener(lambda *_: on_click(btn_frame)))
|
|
return btn_frame
|
|
|
|
repeat_btn_frame = create_rounded_button(R.drawable.msg_retry, "Повторить", on_repeat)
|
|
copy_btn_frame = create_rounded_button(R.drawable.msg_copy, "Копировать", on_copy)
|
|
more_btn_frame = create_rounded_button(R.drawable.ic_ab_other, "", on_more)
|
|
|
|
actions_row.addView(repeat_btn_frame, LayoutHelper.createLinear(-2, -2, Gravity.CENTER_VERTICAL, 0, 0, 6, 0))
|
|
actions_row.addView(copy_btn_frame, LayoutHelper.createLinear(-2, -2, Gravity.CENTER_VERTICAL, 0, 0, 6, 0))
|
|
actions_row.addView(more_btn_frame, LayoutHelper.createLinear(-2, -2, Gravity.CENTER_VERTICAL, 0, 0, 0, 0))
|
|
|
|
root_layout.addView(actions_row, LayoutHelper.createLinear(-1, -2, 0, 0, 0, 12))
|
|
|
|
close_btn_frame = FrameLayout(context)
|
|
close_btn_bg = GradientDrawable()
|
|
close_btn_bg.setCornerRadius(AndroidUtilities.dp(10))
|
|
try:
|
|
btn_color = Theme.getColor(Theme.key_windowBackgroundWhiteBlueText)
|
|
except Exception:
|
|
btn_color = Color.parseColor("#3390EC")
|
|
close_btn_bg.setColor(btn_color)
|
|
|
|
try:
|
|
from android.graphics.drawable import RippleDrawable
|
|
from android.content.res import ColorStateList
|
|
ripple_color = ColorStateList.valueOf(Color.parseColor("#30FFFFFF"))
|
|
ripple_drawable = RippleDrawable(ripple_color, close_btn_bg, None)
|
|
close_btn_frame.setBackground(ripple_drawable)
|
|
except Exception:
|
|
close_btn_frame.setBackground(close_btn_bg)
|
|
|
|
close_btn_frame.setPadding(0, AndroidUtilities.dp(14), 0, AndroidUtilities.dp(14))
|
|
close_btn_frame.setClickable(True)
|
|
close_btn_frame.setFocusable(True)
|
|
|
|
close_btn_text = TextView(context)
|
|
close_btn_text.setText("Закрыть сводку")
|
|
close_btn_text.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16)
|
|
close_btn_text.setTypeface(AndroidUtilities.bold())
|
|
close_btn_text.setGravity(Gravity.CENTER)
|
|
close_btn_text.setTextColor(Color.WHITE)
|
|
|
|
close_btn_frame.addView(close_btn_text, FrameLayout.LayoutParams(-1, -2))
|
|
close_btn_frame.setOnClickListener(OnClickListener(lambda *_: sheet.dismiss()))
|
|
|
|
root_layout.addView(close_btn_frame, LayoutHelper.createLinear(-1, -2, 0, 0, 0, 0))
|
|
|
|
sheet.setCustomView(root_layout)
|
|
sheet.show()
|
|
except Exception as e:
|
|
pass
|
|
|
|
def _show_summary_dialog(self, summary_text: str, message_count: int):
|
|
try:
|
|
fragment = get_last_fragment()
|
|
if not fragment or not hasattr(fragment, 'getParentActivity'):
|
|
BulletinHelper.show_error("Не удалось показать сводку")
|
|
return
|
|
|
|
context = fragment.getParentActivity()
|
|
if not context:
|
|
BulletinHelper.show_error("Не удалось показать сводку")
|
|
return
|
|
|
|
builder = AlertDialogBuilder(context, AlertDialogBuilder.ALERT_TYPE_MESSAGE)
|
|
|
|
title = f"📝 Сводка ({message_count} сообщений)"
|
|
builder.set_title(title)
|
|
builder.set_message(summary_text)
|
|
|
|
builder.set_message_text_view_clickable(True)
|
|
|
|
def copy_to_clipboard(dialog_builder, which):
|
|
try:
|
|
from android.content import ClipData, ClipboardManager, Context
|
|
clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE)
|
|
clip = ClipData.newPlainText("Сводка непрочитанных", summary_text)
|
|
clipboard.setPrimaryClip(clip)
|
|
BulletinHelper.show_success("Скопировано в буфер обмена")
|
|
except Exception as copy_error:
|
|
BulletinHelper.show_error("Ошибка копирования")
|
|
|
|
builder.set_neutral_button("Закрыть", lambda b, w: b.dismiss())
|
|
builder.set_positive_button("Копировать", copy_to_clipboard)
|
|
builder.set_cancelable(True)
|
|
builder.show()
|
|
|
|
BulletinHelper.show_success(f"Проанализировано {message_count} сообщений")
|
|
|
|
except Exception as e:
|
|
BulletinHelper.show_error(f"Ошибка: {str(e)}")
|
|
|
|
def _show_error_in_cell(self, unread_cell, error_text: str):
|
|
def update_cell():
|
|
try:
|
|
text_view = get_private_field(unread_cell, "textView")
|
|
if text_view:
|
|
text_view.setText(error_text)
|
|
else:
|
|
BulletinHelper.show_error(error_text)
|
|
except Exception as e:
|
|
BulletinHelper.show_error(error_text)
|
|
|
|
run_on_ui_thread(update_cell)
|
|
|
|
def _perform_click_vibration(self, view):
|
|
try:
|
|
context = None
|
|
try:
|
|
context = view.getContext()
|
|
while context and not hasattr(context, 'getSystemService'):
|
|
if hasattr(context, 'getBaseContext'):
|
|
context = context.getBaseContext()
|
|
else:
|
|
break
|
|
except Exception:
|
|
pass
|
|
|
|
if not context:
|
|
fragment = get_last_fragment()
|
|
if fragment and hasattr(fragment, 'getParentActivity'):
|
|
context = fragment.getParentActivity()
|
|
|
|
if not context:
|
|
return
|
|
|
|
try:
|
|
from java.lang import Class as JClass
|
|
HFC = JClass.forName("android.view.HapticFeedbackConstants")
|
|
haptic_constant = getattr(HFC, "KEYBOARD_TAP", getattr(HFC, "VIRTUAL_KEY", 1))
|
|
|
|
if hasattr(view, 'performHapticFeedback'):
|
|
view.performHapticFeedback(haptic_constant)
|
|
return
|
|
except Exception as haptic_error:
|
|
pass
|
|
|
|
try:
|
|
vibrator = context.getSystemService("vibrator")
|
|
if vibrator:
|
|
try:
|
|
from java.lang import Class as JClass
|
|
VibrationEffect = JClass.forName("android.os.VibrationEffect")
|
|
effect = VibrationEffect.createOneShot(20, 120)
|
|
vibrator.vibrate(effect)
|
|
except Exception:
|
|
vibrator.vibrate(20)
|
|
except Exception as vib_error:
|
|
pass
|
|
|
|
except Exception as e:
|
|
pass
|
|
|
|
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:
|
|
pass
|
|
|
|
def _show_api_key_dialog(self, view):
|
|
try:
|
|
fragment = get_last_fragment()
|
|
if not fragment or not hasattr(fragment, 'getParentActivity'):
|
|
BulletinHelper.show_error("Cannot get current context")
|
|
return
|
|
|
|
context = fragment.getParentActivity()
|
|
if not context:
|
|
BulletinHelper.show_error("Cannot get current context")
|
|
return
|
|
|
|
from org.telegram.messenger import AndroidUtilities
|
|
|
|
current_value = self.get_setting("gemini_api_key", "")
|
|
|
|
edit_text = EditTextBoldCursor(context)
|
|
edit_text.setText(current_value)
|
|
edit_text.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)
|
|
edit_text.setHint(locali.get_string("API_KEY_SUBTEXT"))
|
|
edit_text.setSingleLine(True)
|
|
|
|
try:
|
|
from android.graphics.drawable import ColorDrawable
|
|
from android.content.res import ColorStateList
|
|
from android.graphics import Color
|
|
|
|
edit_text.setTextColor(Theme.getColor(Theme.key_dialogTextBlack))
|
|
edit_text.setHintTextColor(Theme.getColor(Theme.key_dialogTextGray3))
|
|
edit_text.setLineColors(
|
|
Theme.getColor(Theme.key_dialogInputField),
|
|
Theme.getColor(Theme.key_dialogInputFieldActivated),
|
|
Theme.getColor(Theme.key_text_RedBold)
|
|
)
|
|
edit_text.setCursorColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText))
|
|
edit_text.setBackground(ColorDrawable(0))
|
|
edit_text.setBackgroundTintList(ColorStateList.valueOf(Color.TRANSPARENT))
|
|
except Exception:
|
|
pass
|
|
|
|
padding_h = AndroidUtilities.dp(20)
|
|
padding_v = AndroidUtilities.dp(12)
|
|
edit_text.setPadding(padding_h, padding_v, padding_h, padding_v)
|
|
|
|
builder = AlertDialogBuilder(context, AlertDialogBuilder.ALERT_TYPE_MESSAGE)
|
|
builder.set_title(locali.get_string("API_KEY_INPUT"))
|
|
builder.set_view(edit_text)
|
|
|
|
def on_ok_click(dialog_builder, which):
|
|
new_value = str(edit_text.getText()).strip()
|
|
self.set_setting("gemini_api_key", new_value)
|
|
dialog_builder.dismiss()
|
|
BulletinHelper.show_success("API ключ сохранен" if locali.language == "ru" else "API key saved")
|
|
|
|
builder.set_positive_button("OK", on_ok_click)
|
|
builder.set_negative_button(locali.get_string("MENU_CANCEL") if locali.language == "ru" else "Cancel", lambda b, w: b.dismiss())
|
|
builder.show()
|
|
|
|
def focus_edit():
|
|
edit_text.requestFocus()
|
|
from android.view.inputmethod import InputMethodManager
|
|
from android.content import Context
|
|
imm = context.getSystemService(Context.INPUT_METHOD_SERVICE)
|
|
if imm:
|
|
imm.showSoftInput(edit_text, InputMethodManager.SHOW_IMPLICIT)
|
|
|
|
from android_utils import R
|
|
run_on_ui_thread(lambda: edit_text.post(R(focus_edit)))
|
|
|
|
except Exception as e:
|
|
BulletinHelper.show_error(f"Error: {str(e)}")
|
|
|
|
def _show_openai_api_key_dialog(self, view):
|
|
try:
|
|
fragment = get_last_fragment()
|
|
if not fragment or not hasattr(fragment, 'getParentActivity'):
|
|
BulletinHelper.show_error("Cannot get current context")
|
|
return
|
|
|
|
context = fragment.getParentActivity()
|
|
if not context:
|
|
BulletinHelper.show_error("Cannot get current context")
|
|
return
|
|
|
|
from org.telegram.messenger import AndroidUtilities
|
|
|
|
current_value = self.get_setting("openai_api_key", "")
|
|
|
|
edit_text = EditTextBoldCursor(context)
|
|
edit_text.setText(current_value)
|
|
edit_text.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)
|
|
edit_text.setHint(locali.get_string("OPENAI_API_KEY_INPUT"))
|
|
edit_text.setSingleLine(True)
|
|
|
|
try:
|
|
from android.graphics.drawable import ColorDrawable
|
|
from android.content.res import ColorStateList
|
|
from android.graphics import Color
|
|
|
|
edit_text.setTextColor(Theme.getColor(Theme.key_dialogTextBlack))
|
|
edit_text.setHintTextColor(Theme.getColor(Theme.key_dialogTextGray3))
|
|
edit_text.setLineColors(
|
|
Theme.getColor(Theme.key_dialogInputField),
|
|
Theme.getColor(Theme.key_dialogInputFieldActivated),
|
|
Theme.getColor(Theme.key_text_RedBold)
|
|
)
|
|
edit_text.setCursorColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText))
|
|
edit_text.setBackground(ColorDrawable(0))
|
|
edit_text.setBackgroundTintList(ColorStateList.valueOf(Color.TRANSPARENT))
|
|
except Exception:
|
|
pass
|
|
|
|
padding_h = AndroidUtilities.dp(20)
|
|
padding_v = AndroidUtilities.dp(12)
|
|
edit_text.setPadding(padding_h, padding_v, padding_h, padding_v)
|
|
|
|
builder = AlertDialogBuilder(context, AlertDialogBuilder.ALERT_TYPE_MESSAGE)
|
|
builder.set_title(locali.get_string("OPENAI_API_KEY_INPUT"))
|
|
builder.set_view(edit_text)
|
|
|
|
def on_ok_click(dialog_builder, which):
|
|
new_value = str(edit_text.getText()).strip()
|
|
self.set_setting("openai_api_key", new_value)
|
|
dialog_builder.dismiss()
|
|
BulletinHelper.show_success("API ключ сохранен" if locali.language == "ru" else "API key saved")
|
|
|
|
builder.set_positive_button("OK", on_ok_click)
|
|
builder.set_negative_button(locali.get_string("MENU_CANCEL") if locali.language == "ru" else "Cancel", lambda b, w: b.dismiss())
|
|
builder.show()
|
|
|
|
def focus_edit():
|
|
edit_text.requestFocus()
|
|
from android.view.inputmethod import InputMethodManager
|
|
from android.content import Context
|
|
imm = context.getSystemService(Context.INPUT_METHOD_SERVICE)
|
|
if imm:
|
|
imm.showSoftInput(edit_text, InputMethodManager.SHOW_IMPLICIT)
|
|
|
|
from android_utils import R
|
|
run_on_ui_thread(lambda: edit_text.post(R(focus_edit)))
|
|
|
|
except Exception as e:
|
|
BulletinHelper.show_error(f"Error: {str(e)}")
|
|
|
|
def _show_prompt_dialog(self, view):
|
|
try:
|
|
fragment = get_last_fragment()
|
|
if not fragment or not hasattr(fragment, 'getParentActivity'):
|
|
BulletinHelper.show_error("Cannot get current context")
|
|
return
|
|
|
|
context = fragment.getParentActivity()
|
|
if not context:
|
|
BulletinHelper.show_error("Cannot get current context")
|
|
return
|
|
|
|
from org.telegram.messenger import AndroidUtilities
|
|
from android.widget import ScrollView
|
|
from android.util import TypedValue
|
|
|
|
default_prompt = self._get_default_prompt()
|
|
current_value = self.get_setting("summary_prompt", default_prompt)
|
|
|
|
scroll_view = ScrollView(context)
|
|
|
|
edit_text = EditTextBoldCursor(context)
|
|
edit_text.setText(current_value)
|
|
edit_text.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
|
|
edit_text.setHint("Введите промпт для суммаризации..." if locali.language == "ru" else "Enter summarization prompt...")
|
|
edit_text.setMaxLines(15)
|
|
edit_text.setSingleLine(False)
|
|
edit_text.setVerticalScrollBarEnabled(True)
|
|
edit_text.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14)
|
|
|
|
try:
|
|
from android.graphics.drawable import ColorDrawable
|
|
from android.content.res import ColorStateList
|
|
from android.graphics import Color
|
|
|
|
edit_text.setTextColor(Theme.getColor(Theme.key_dialogTextBlack))
|
|
edit_text.setHintTextColor(Theme.getColor(Theme.key_dialogTextGray3))
|
|
edit_text.setLineColors(
|
|
Theme.getColor(Theme.key_dialogInputField),
|
|
Theme.getColor(Theme.key_dialogInputFieldActivated),
|
|
Theme.getColor(Theme.key_text_RedBold)
|
|
)
|
|
edit_text.setCursorColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText))
|
|
edit_text.setBackground(ColorDrawable(0))
|
|
edit_text.setBackgroundTintList(ColorStateList.valueOf(Color.TRANSPARENT))
|
|
except Exception:
|
|
pass
|
|
|
|
padding = AndroidUtilities.dp(20)
|
|
edit_text.setPadding(padding, padding, padding, padding)
|
|
|
|
scroll_view.addView(edit_text)
|
|
|
|
builder = AlertDialogBuilder(context, AlertDialogBuilder.ALERT_TYPE_MESSAGE)
|
|
builder.set_title(locali.get_string("SUMMARY_PROMPT_INPUT"))
|
|
builder.set_view(scroll_view)
|
|
|
|
def on_ok_click(dialog_builder, which):
|
|
new_value = str(edit_text.getText()).strip()
|
|
if new_value:
|
|
self.set_setting("summary_prompt", new_value)
|
|
dialog_builder.dismiss()
|
|
BulletinHelper.show_success("Промпт сохранен" if locali.language == "ru" else "Prompt saved")
|
|
else:
|
|
BulletinHelper.show_error("Промпт не может быть пустым" if locali.language == "ru" else "Prompt cannot be empty")
|
|
|
|
builder.set_positive_button("OK", on_ok_click)
|
|
builder.set_negative_button(locali.get_string("MENU_CANCEL") if locali.language == "ru" else "Cancel", lambda b, w: b.dismiss())
|
|
|
|
def on_reset_click(dialog_builder, which):
|
|
edit_text.setText(default_prompt)
|
|
BulletinHelper.show_info("Промпт сброшен" if locali.language == "ru" else "Prompt reset")
|
|
|
|
builder.set_neutral_button("Сброс" if locali.language == "ru" else "Reset", on_reset_click)
|
|
builder.show()
|
|
|
|
def focus_edit():
|
|
edit_text.requestFocus()
|
|
edit_text.setSelection(len(str(edit_text.getText())))
|
|
from android.view.inputmethod import InputMethodManager
|
|
from android.content import Context
|
|
imm = context.getSystemService(Context.INPUT_METHOD_SERVICE)
|
|
if imm:
|
|
imm.showSoftInput(edit_text, InputMethodManager.SHOW_IMPLICIT)
|
|
|
|
from android_utils import R
|
|
run_on_ui_thread(lambda: edit_text.post(R(focus_edit)))
|
|
|
|
except Exception as e:
|
|
BulletinHelper.show_error(f"Error: {str(e)}")
|
|
|
|
def create_settings(self):
|
|
provider_type = self.get_setting("provider_type", 0)
|
|
try:
|
|
provider_type = int(provider_type)
|
|
except (ValueError, TypeError):
|
|
provider_type = 0
|
|
|
|
items = [
|
|
Header(text=locali.get_string("AI_SETTINGS_HEADER")),
|
|
Selector(
|
|
key="provider_type",
|
|
text=locali.get_string("PROVIDER_SELECTOR"),
|
|
icon="msg_bot",
|
|
default=provider_type,
|
|
items=PROVIDER_TYPES
|
|
),
|
|
]
|
|
|
|
if provider_type == 0:
|
|
items.extend([
|
|
Text(
|
|
text=locali.get_string("API_KEY_INPUT"),
|
|
icon="msg_pin_code",
|
|
on_click=self._show_api_key_dialog
|
|
),
|
|
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(),
|
|
Selector(
|
|
key="model_selection",
|
|
text=locali.get_string("MODEL_SELECTOR"),
|
|
icon="msg_language_solar",
|
|
default=1,
|
|
items=MODEL_DISPLAY_NAMES
|
|
),
|
|
])
|
|
else:
|
|
items.extend([
|
|
Header(text=locali.get_string("OPENAI_SECTION")),
|
|
Text(
|
|
text=locali.get_string("OPENAI_API_KEY_INPUT"),
|
|
icon="msg_pin_code",
|
|
on_click=self._show_openai_api_key_dialog
|
|
),
|
|
Input(
|
|
key="openai_base_url",
|
|
text=locali.get_string("OPENAI_BASE_URL_INPUT"),
|
|
icon="msg_link",
|
|
default="https://api.openai.com"
|
|
),
|
|
Input(
|
|
key="openai_model",
|
|
text=locali.get_string("OPENAI_MODEL_INPUT"),
|
|
icon="msg_language_solar",
|
|
default="gpt-5-chat"
|
|
),
|
|
Input(
|
|
key="openai_api_key_header",
|
|
text=locali.get_string("OPENAI_HEADER_NAME_INPUT"),
|
|
icon="msg_settings",
|
|
default="Authorization"
|
|
),
|
|
Input(
|
|
key="openai_api_key_prefix",
|
|
text=locali.get_string("OPENAI_HEADER_PREFIX_INPUT"),
|
|
icon="msg_settings",
|
|
default="Bearer"
|
|
),
|
|
Divider(),
|
|
])
|
|
|
|
items.extend([
|
|
Selector(
|
|
key="summary_language",
|
|
text="Summary Language" if locali.language == "en" else "Язык сводки",
|
|
icon="msg_translate",
|
|
default=0,
|
|
items=["🇷🇺 Русский", "🇬🇧 English"]
|
|
),
|
|
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="256000",
|
|
subtext=locali.get_string("MAX_TOKENS_SUBTEXT")
|
|
),
|
|
Input(
|
|
key="message_limit",
|
|
text=locali.get_string("MESSAGE_LIMIT_INPUT"),
|
|
icon="msg_data",
|
|
default="0",
|
|
subtext=locali.get_string("MESSAGE_LIMIT_SUBTEXT")
|
|
),
|
|
Divider(),
|
|
Header(text=locali.get_string("PROMPT_HEADER")),
|
|
Text(
|
|
text=locali.get_string("SUMMARY_PROMPT_INPUT"),
|
|
icon="msg_edit",
|
|
on_click=self._show_prompt_dialog
|
|
),
|
|
])
|
|
|
|
return items |